2009-05-31

C-Max CMMR-6P-60 to Human Readable

What's not to like about a string of zeros and ones? According to Futurama, in Robot Church, two is the same as Amen. And with that oddball thought disposed of, the next step is to provide something readable by something besides a robot.

If the bit order is correct, you can stuff a variable containing your 8-bit BCD into the input of a function like the following and have it spit back a decimal number:

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


This works really well if you are receiving the DCF-77 time signal code. The DCF-77 code transmits all its data in 3 to 8 bit BCD data points that start with the least significant bit and end with the most significant bit. The code merely has to parse out the section we're interested in, submit it to our "BCD to Decimal" function and we get back a meaningful number. Minute, Hour, Day, Month and Year are directly accessible.

The updated WWV/WWVH time code format is almost as easy to decode. At least the bit order is right. Minute and Hour are nearly directly accessible, they have a Zero bit between the tens and ones that will have to be dropped. The major problems are surmountable. First, there is a year tens nibble at the beginning that needs to be added to the year ones nibble at the end of the data stream. Second, if you are wanting a direct month and day readout, you will have to calculate it. US time codes transmit a "DAY OF YEAR" and a "Leapyear Flag". The "Day of Year" is 10 bits so it will have to be broken into two bytes to enable our function to decode it.

And then we get to WWVB time code format which reverses the bit order. The positions make sense if you were reading a bank of lamps as the BCD bit order follows natural decimal place order.

One method of decoding WWVB BCD time format consists of finding the double mark frame reference and then doing a bank of if statements and compound addition to decode the decimal data. The following code follows the natural decimal place order, accumulates the addition and spits out the numbers. For a simple "display UTC as it happens" it's pretty efficient as it accumulates the time and notifies you 20 seconds after the minute marker.

      minutes = 0;                      // set minutes variable to zero
if (pulseValue()) minutes = 40; // bit 01 min 40
if (pulseValue()) minutes += 20; // bit 02 min 20
if (pulseValue()) minutes += 10; // bit 03 min 10
if (pulseValue()); // bit 04 spacer
if (pulseValue()) minutes += 8; // bit 05 min 8
if (pulseValue()) minutes += 4; // bit 06 min 4
if (pulseValue()) minutes += 2; // bit 07 min 2
if (pulseValue()) minutes += 1; // bit 08 min 1

if (pulseValue()); // bit 09 marker P1 09 Seconds
if (pulseValue()); // bit 10 blank
if (pulseValue()); // bit 11 blank

hours = 0; // set hours variable to zero
if (pulseValue()) hours = 20; // bit 12 hour 20
if (pulseValue()) hours += 10; // bit 13 hour 10
if (pulseValue()); // bit 14 spacer
if (pulseValue()) hours += 8; // bit 15 hour 8
if (pulseValue()) hours += 4; // bit 16 hour 4
if (pulseValue()) hours += 2; // bit 17 hour 2
if (pulseValue()) hours += 1; // bit 18 hour 1
printTime();



The frame reject code produces a bit count that we can combine with a bit mask of sorts to eliminate the blank bits and marks from our decoded BCD.

// 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
};


The next step is to reverse the bit order on the received data so that the bcdToDec() function can use the input variable contents to generate the number. Using shift left << and a shift counter that decrements from 7, we can stuff a byte variable with each decoded bit. It looks something like this for decoding minutes:

    else if ((bitCount <  9) && (bitMask == 1)) { // minutes 7 bits
mns = mns | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter
}


A frameReject() resets all the counters and buffers to zero and lets us start again.

Upon getting the correct bit count and frame count triggers the frameAccept() which finishes out with a shift right >> to byte align our time and date data that are less than eight bits. Once again for minutes which is seven bits, shift right one bit:

  mns = bcdToDec(mns >> 1);     // minutes 7 bits


One item I've noticed that never seems to get addressed is future prediction. We receive a frame reference, then the next minute is spent telling us history about that marker that happened. By the time we get a time readout, a minute has already passed. Let's fix that:

  /* 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;
}


And then to do something about that annoying "Day of Year". When's the last time you've told someone to meet you at Day 168 Leapyear 1 and had them instantly recognize what you meant. Hmm, I didn't think so!

First we need to know the month end dates. This is easily contained by a multi-dimensional array with the first field representing a normal year and the second field a leapyear:

// 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
};



Now by knowing the "Day of Year" and the value of the leapyear flag, the following bit of code can tell us the month and day:

  /* 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++;
}


Now the rest is acdemic. Some simple formatting and printing gives Time UTC and a Month, Day, Year display. I've left all the debug code in the current copy of the sketch so you can see the different serial terminal outputs used to debug this into its final current working version. If you have a serial LCD display, it currently displays UTC time, a non-functioning signal quality indicator and the date.


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

//#define DEBUG // uncomment to dump debug data to serial
//#define DEBUG_BITSTREAM // uncomment to show bits decoded
//#define DEBUG_BCD // uncomment to show BCD

// 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 < 350) { // 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 < 650) { // 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() {
boolean bitMask = wwvbData[bitCount]; // more efficient better than reading
// array on eachtest below

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

#ifdef DEBUG_BCD
nop(bitCount, bitVal);
#endif
// nothing
}
else if ((bitCount < 9) && (bitMask == 1)) { // minutes 7 bits
mns = mns | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter

#ifdef DEBUG_BCD
debugBCD(bitCount, bitVal);
#endif
}
else if ((bitCount < 19) && (bitMask == 1)) { // hours 6 bits
hrs = hrs | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter

#ifdef DEBUG_BCD
debugBCD(bitCount, bitVal);
#endif
}
else if ((bitCount < 24) && (bitMask == 1)) { // day of year 10 bits
doyhi = doyhi | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter

#ifdef DEBUG_BCD
debugBCD(bitCount, bitVal);
#endif
}
else if ((bitCount < 34) && (bitMask == 1)) { // day of year 10 bits
doylo = doylo | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter

#ifdef DEBUG_BCD
debugBCD(bitCount, bitVal);
#endif
}
else if (bitCount < 45) {

#ifdef DEBUG_BCD
nop(bitCount, bitVal);
#endif
}
else if ((bitCount < 54) && (bitMask == 1)) { // year 8 bits
yrs = yrs | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter

#ifdef DEBUG_BCD
debugBCD(bitCount, bitVal);
#endif
}
else if ((bitCount == 55) && (bitMask == 1)) { // leapyear 1 bit
lyr = bitVal; // set leapyear flag

#ifdef DEBUG_BCD
debugBCD(bitCount, bitVal);
#endif
}
else if ((bitCount == 56) && (bitMask == 1)) {
#ifdef DEBUG_BCD
nop(bitCount, bitVal);
#endif
}
else if ((bitCount < 59) && (bitMask == 1)) { // daylight savings time 2 bits
dst = dst | (bitVal << bitShift); // bitstuff in reverse
bitShift --; // decrement counter

#ifdef DEBUG_BCD
debugBCD(bitCount, bitVal);
#endif
}
else {
#ifdef DEBUG_BCD
nop(bitCount, bitVal);
#endif
}
}

#ifdef DEBUG_BITSTREAM
// Display bit values to terminal screen, output delimited data stream with
// colons at mark and new line at frame start. DEBUG ROUTINES
if ((bitVal == 0) && (prevMark == 1)) { // show marks as colon
Serial.print(" : ");
}
else {
Serial.print(bitVal, DEC); // show zero or one
}
#endif
}


#ifdef DEBUG_BCD
int debugBCD(word bitNum, boolean val) {
//if (bitNum < 10) { Serial.print("0"); }
//Serial.print(bitNum, DEC);
//Serial.print(":");
Serial.print(val, DEC);
//Serial.print(" ");
}

int nop(word bitNum, boolean val) {
//if (bitNum < 10) { Serial.print("0"); }
//Serial.print(bitNum, DEC);
//Serial.print(":");
Serial.print(" ");
}
#endif

/******************************************************************************
* 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;

#ifdef DEBUG
// Display Frame Accept message, mark count bit count and sequential number
// of frame errors. DEBUG ROUTINES
Serial.print("\n ->Accepted<- Data - Marks: ");
Serial.print(markCount, DEC);
Serial.print(" Bits: ");
if (bitCount < 10) {
Serial.print("0");
}
Serial.print(bitCount, DEC);
Serial.print(" Frame Errors: ");
if (errorCount < 10) {
Serial.print("0");
}
Serial.println(errorCount, DEC);
#endif
}

/******************************************************************************
* 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;

#ifdef DEBUG
// Display Frame Reject message, mark count bit count and sequential number
// of frame errors. DEBUG ROUTINES
Serial.print("\n ->Scratch<- Bad Data - Marks: ");
Serial.print(markCount, DEC);
Serial.print(" Bits: ");
if (bitCount < 10) {
Serial.print("0");
}
Serial.print(bitCount, DEC);
Serial.print(" Frame Errors: ");
if (errorCount < 10) {
Serial.print("0");
}
Serial.println(errorCount, DEC);
#endif
}

// 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);
}

No comments:

Post a Comment