2009-05-31

Wheel Reinvention and a Change of Course

When I first got this CMMR-6P-60, I forged ahead and tried all the available libraries to see if any of the DCF-77 code would work. Because of the complete mismatch in how time is encoded between DCF-77 and WWVB, they failed miserably. I decided semi start from scratch and got as far as a readable time format and some variables that could be used to prime some sort of clock.

Sources for ideas in attempting this were Rudi Niemeijer's Arduino with DCF-77 Receiver project, Mathias Dalheimer's Arduino DCF77 radio clock receiver and from there back to the source, captain's Atmel ATmega (ATmega16) DCF77 time signal decoder.

Things got overly complex in the area of decoding BCD with all the branching logic for the bit mask and bit shifting to reverse the bit order. In captain's program, there's this thing sitting there:

struct DCF77 {
unsigned long long bits :16; // bits 1 to 16: reserved or not needed here
unsigned long long mez_mesz :1; // 1 at transition from MEZ to MESZ or vice versa
unsigned long long zone1 :1; // 0=MEZ, 1=MESZ
unsigned long long zone2 :1; // 0=MESZ, 1=MEZ
unsigned long long leapsecond :1; // If 1, a leap-second is inserted at end of hour
unsigned long long start :1; // start bit is always 1
unsigned long long min :7; // 7 bits for minutes
unsigned long long parity_min :1; // parity bit for minutes
unsigned long long hour :6; // 6 bits for hour
unsigned long long parity_hour :1; // parity bit for hour
unsigned long long day :6; // 6 bits for day
unsigned long long weekday :3; // 3 bits for weekday
unsigned long long month :5; // 5 bits for month
unsigned long long year :8; // 8 bits for year (5 = 2005)
unsigned long long parity_date :1; // parity bit for date
};


I had to sit down and figure out what was going on and then realized that the simpler method had already been done, I just didn't understand what was being done here.

There is this 64 bit buffer:

unsigned long long dcf77_buffer = 0;


That gets data added to it like this:

dcf77_buffer = dcf77_buffer | ((unsigned long long) 1 << bit_counter);


which is pretty familiar as I've been doing it in reverse to get my bit order correct to decode BCD, except on a much smaller scale.

So after stuffing everything into a 64 bit buffer, how do you get it out? The following string
tmp_buffer = (struct DCF77 *)(unsigned long long)&dcf77_buffer;

is a pointer construct that ties the DCF77 data structure to the dcf77_buffer. The (unsigned long long) is a cast that makes sure we're addressing the buffer with the right data type.

To figure out how this works, you will have to do a little c++ research beyond the Arduino Pointer page. This excerpt from Practical C Programming is a start.

So, here's my final version with all the debug code stripped out:

Clock WWVB Data Decode BCD

/**********************************************************************
* Clock WWVB Data Decode BCD by capt.tagon
*
* WWVB receiver input on digital pin 2
**********************************************************************/

#define wwvbIn 2 // WWVB receiver data input digital pin
#define ledRxPin 4 // WWVB receiver state indicator pin
#define ledFramePin 6 // Data received frame indicator pin
#define ledBitPin 5 // LED data decoded indicator pin
#define ledMarkPin 7 // Data received mark inicator pin

// variable changed by interrupt service routine - volatile
volatile byte wwvbInState; // store receiver signal level

// variables for signal level and pulse duration timing
byte prevWwvbInState; // store previous signal level
unsigned int prevEdgeMillis; // store time signal was read

// variables for basic data integrity and frame rejection
boolean bitVal; // bit decoded 0, 1, Mark or Frame
boolean badBit = 0; // bad bit, noise detected
byte markCount = 0; // mark count, 6 pulses per minute
boolean prevMark = 0; // store previous mark
byte bitCount = 0; // bits, 60 pulses per minute
boolean frameError = 0; // set for frame reject
word errorCount = 0; // keep count of frame errors


// WWVB transmitted data bit mask. 0nes indicate data, zeros are marks and blanks
boolean wwvbData[60] = { // final bit in row is mark bit
0, 1, 1, 1, 0, 1, 1, 1, 1, 0, // 9 mark P1
0, 0, 1, 1, 0, 1, 1, 1, 1, 0, // 19 mark P2
0, 0, 1, 1, 0, 1, 1, 1, 1, 0, // 29 mark P3
1, 1, 1, 1, 0, 0, 1, 1, 1, 0, // 39 mark P4
1, 1, 1, 1, 0, 1, 1, 1, 1, 0, // 49 mark P5
1, 1, 1, 1, 0, 1, 1, 1, 1, 0, // 59 mark P0
};

// decoded BCD time and date buffers. Byte variables are bitstuffed,
// shifted and then BCD decoded to Decimal. Not for use as clock input
// as they will contain data of an indeterminite value.
byte bitShift; // shift counter to reverse bit order
byte mns; // minutes 7 bits
byte hrs; // hours 6 bits
byte doyhi; // day high byte 2 bits
byte doylo; // day low byte 8 bits
word doy; // decimal day of year
byte yrs; // years 8 bits
boolean lyr; // leapyear 1 bit
byte dst; // daylight savings time 2 bits

// Volatile variables for clock input. Will be changed at any time as
// a result of a chain of events precipitated by readlevel()
volatile byte mm;
volatile byte hh;
volatile byte MM;
volatile byte DD;
volatile word YYYY;

// End of Month - to calculate Month and Day from Day of Year
int eomYear[13][2] = {
{ 0,0 }, // Begin
{ 31,31 }, // Jan
{ 59,60 }, // Feb
{ 90,91 }, // Mar
{ 120,121 }, // Apr
{ 151,152 }, // May
{ 181,182 }, // Jun
{ 212,213 }, // Jul
{ 243,244 }, // Aug
{ 273,274 }, // Sep
{ 304,305 }, // Oct
{ 334,335 }, // Nov
{ 365,366 } // Dec
};


/* Standard Arduino setup() function */

void setup() {
pinMode(wwvbIn, INPUT);
pinMode(ledRxPin, OUTPUT);
pinMode(ledFramePin, OUTPUT);
pinMode(ledBitPin, OUTPUT);
pinMode(ledMarkPin, OUTPUT);
attachInterrupt(0, readLevel, CHANGE); // fire interrupt on edge detected
Serial.begin(9600);
lcdInit();
}

/* Standard Arduino loop() function */

void loop() {
if (wwvbInState != prevWwvbInState) {
pulseValue();
prevWwvbInState = wwvbInState;
}
}

/******************************************************************
* pulseValue()
*
* determine pulse width 200ms = 0, 500ms = 1, 800ms = mark
******************************************************************/

void pulseValue() {
unsigned int edgeMillis = millis(); // save current time
if (wwvbInState == 1) { // rising edge
prevEdgeMillis = edgeMillis; // set previous time to current
}
else { // falling edge
int pulseLength = edgeMillis - prevEdgeMillis; // calculate pulse length millis
badBit = 0; // clear bad bit detected
digitalWrite(ledMarkPin, LOW);
digitalWrite(ledFramePin, LOW);
if (pulseLength < 100) { // less than 100ms, noise pulses
badBit = 1; // bad bit, signal pulse noise
}
else if (pulseLength < 400) { // 800ms carrier drop mark
// two sequential marks -> start of frame. If we read 6 marks and 60 bits
// (0-59), we should have received a valid frame
if ((prevMark == 1) && (markCount == 6) && (bitCount == 59)) {
bitVal = 0;
frameAccept(); // data decoded, accept frame
digitalWrite(ledFramePin, HIGH); // frame received, ready for new frame
markCount = 0; // start counting marks, 6 per minute
prevMark = 0; // set bit counter to one
bitCount = 0; // should be a valid frame
frameError = 0; // set frame error indicator to zero
errorCount = 0; // set frame error count to zero
}
else if ((prevMark == 1) && ((markCount != 6) || (bitCount != 59))) {
errorCount ++; // increment frame error count
frameReject(); // bad decode, reject frame data
digitalWrite(ledFramePin, HIGH); // apparent frame, wrong mark and bit count
markCount = 0; // bad start of frame set mark count to zero
prevMark = 0; // clear previous to restart frame
bitCount = 0; // set bit count to one
frameError = 1; // and indicate frame error
}
else { // 10 second marker
bitVal = 0;
markCount ++; // increment mark counter, 6 per minute
digitalWrite(ledMarkPin, HIGH); // mark received
prevMark = 1; // set mark state to one, following mark indicates frame
bitCount ++; // increment bit counter
}
}
else if (pulseLength < 700) { // 500ms carrier drop one
bitVal = 1;
digitalWrite(ledBitPin, HIGH); // bit indicator LED on, one received
prevMark = 0; // set mark counter to zero
bitCount ++; // increment bit counter
}
else { // 200ms carrier drop zero
bitVal = 0;
digitalWrite(ledBitPin, LOW); // bit indicator LED off, zero received
prevMark = 0; // set mark counter to zero
bitCount ++; // increment bit counter
}
if (badBit == 0) { // reject noise
timeDateDecode();
}
}
}

/******************************************************************************
* readLevel() {
*
* Pin 2 INT0 Interrupt Handler Reads pin state - flashes signal indicator LED
******************************************************************************/

void readLevel() {
wwvbInState = digitalRead(wwvbIn); // read signal level
digitalWrite(ledRxPin, wwvbInState); // flash WWVB receiver indicator pin
}

/******************************************************************************
* timeDateDecode()
*
* Decode function to extract BCD from data stream
******************************************************************************/

void timeDateDecode() {
// bitMask variable, better than reading array on each test below
boolean bitMask = wwvbData[bitCount];

if ((bitCount == 9) || (bitCount == 19) || (bitCount == 24) ||
(bitCount == 34) || (bitCount == 54) || (bitCount == 59)) {
bitShift = 7; // reset shift counter
}
else {

if (bitMask == 0) { // ignore blanks and marks
// nothing
}
else if ((bitCount < 9) && (bitMask == 1)) { // minutes 7 bits
mns = mns | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter
}
else if ((bitCount < 19) && (bitMask == 1)) { // hours 6 bits
hrs = hrs | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter
}
else if ((bitCount < 24) && (bitMask == 1)) { // day of year 10 bits
doyhi = doyhi | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter
}
else if ((bitCount < 34) && (bitMask == 1)) { // day of year 10 bits
doylo = doylo | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter
}
else if (bitCount < 45) {
// nothing
}
else if ((bitCount < 54) && (bitMask == 1)) { // year 8 bits
yrs = yrs | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter
}
else if ((bitCount == 55) && (bitMask == 1)) { // leapyear 1 bit
lyr = bitVal; // set leapyear flag
}
else if ((bitCount == 56) && (bitMask == 1)) {
// nothing
}
else if ((bitCount < 59) && (bitMask == 1)) { // daylight savings time 2 bits
dst = dst | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter
}
else {
// nothing
}
}
}

/******************************************************************************
* frameAccept()
*
* Accept function for completed frame decode converts recovered BCD to decimal,
* makes conversions and saves buffer contents to clock variables
******************************************************************************/

void frameAccept() {
mns = bcdToDec(mns >> 1); // minutes 7 bits
hrs = bcdToDec(hrs >> 2); // hours 6 bits
doy = (100 * bcdToDec(doyhi >> 6)) + bcdToDec(doylo);
yrs = bcdToDec(yrs); // years 8 bits
dst = bcdToDec(dst >> 6); // daylight savings time 2 bits

/* date/time data takes one full minute to transmit after its frame reference bit has been
* received. Add one minute to synch with our clock with the following frame reference to
* WWVB time.
*/
if (mns == 59) { // make clock time synch with following frame marker
mm = 0;
hh = (hrs + 1 + 24) % 24; // future minute is new hour
}
else {
mm = mns + 1; // else future minute
hh = hrs;
}

/* DCF77 sends month and day information, we aren't so lucky.
* WWVB only sends Day of Year, Month and Day will need to be calculated, and 02/29 added for leapyear.
* We're given Day of Year, compare against month ending day and calculate month and day.
* Use leapyear flag to add one day to February.
*/
int eom = 0; // eom counter used to determine month and day
while (eomYear[eom][lyr] < doy) { // calculate month and day for UTC
DD = doy - eomYear[eom][lyr];
MM = (eom + 1) % 12;
eom++;
}

YYYY = 2000 + yrs;

printTime();


// clear buffers for next WWVB reading
mns = 0;
hrs = 0;
doyhi = 0;
doylo = 0;
yrs = 0;
lyr = 0;
dst = 0;
}

/******************************************************************************
* frameReject()
*
* Reject function for bad frame decode, reset all buffers to zero, we're going
* for a restart
******************************************************************************/

void frameReject() {
mns = 0;
hrs = 0;
doyhi = 0;
doylo = 0;
yrs = 0;
lyr = 0;
dst = 0;
}

// convert BCD to decimal
word bcdToDec (word val) {
val = (val/16*10) + (val%16);
return(val);
}

/*****************************************************************************
* Time display functions
*****************************************************************************/

// LCD routines to initialize LCD and clear screen
void lcdInit() { // using P H Anderson Serial LCD driver board
Serial.print("?G216"); // configure driver for 2 x 16 LCD
delay(300);
Serial.print("?BDD"); // set backlight brightness
delay(300);
Serial.print("?f"); // clear screen
delay(300);
Serial.print("?D00000000000001F1F"); // special character low bar
delay(300);
Serial.print("?D10000001F1F1F1F1F"); // special character half block
delay(300);
Serial.print("?D21F1F1F1F1F1F1F1F"); // special character full block
delay(300);
Serial.print("?c0"); // set cursor off
}

void printTime() {

Serial.print("?f"); // clear screen
Serial.print("?x00?y0");
if (hh < 10) {
Serial.print("0");
}
Serial.print(hh, DEC);
Serial.print(":");
if (mm < 10) {
Serial.print("0");
}
Serial.print(mm, DEC);
Serial.print(" ");
Serial.print("UTC");
Serial.print(" ");

/******************************************************************************************
* frame error indicator - serves as signal indicator
******************************************************************************************/
if (frameError == 1 && errorCount < 5) { // five or less sequential frame errors
Serial.print("?1");
}
else if (frameError == 1 & errorCount >= 5) { // more than 5 frame errors
Serial.print("?0");
}
else {
Serial.print("?2"); // clear signal reception
}

Serial.print("?x00?y1");
if (MM < 10) {
Serial.print("0");
}
Serial.print(MM, DEC);
Serial.print("-");
if (DD < 10) {
Serial.print("0");
}
Serial.print(DD, DEC);
Serial.print("-");
Serial.print(YYYY, DEC);
}


The next step is to take what I've put together, do a little machining and shimming and see how it can be made to match up to some already existing code.

No comments:

Post a Comment