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.

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

2009-05-28

Decoding WWVB Time Data -> Frame Reject

Last visit here, our sketch was merrily finding rising and falling edges to the pulse and measuring the duration between the rise and the fall. That then resolved itself into a data stream that sometimes has odd results when noise spikes and signal dropouts occur.

We have few tools to work with here for detecting errors. There are three main features to the signal, (1) a frame reference pulse 1 ppm, 10 second indicators 6 ppm and a complete bit count of 60 bits (tri state, with values of mark, 1 and 0, so not really bit in the binary sense).

Noise works with us in a way, because it usually mangles marks, turns ones into marks and zeros into ones, so if we set up (1) a bit counter, (2) a mark counter, and (3) a previous mark flag, we can sort things out by keeping track of our frame reference, and the number of marks and bits received.

10 second markers are lone 200ms pulses from TCO (Time Clock Out). Bit 59 is a mark, followed by bit 0 which is Pr. This two mark sequence signals our start of a new data frame, starting with bit 0. Here's where our previous mark flag comes into action. Every time a mark is received, the flag is set, the following 0 or 1 unsets it. By counting the marks and bits, we can use the following logic for a frame accept:

if ((prevMark == 1) && (markCount == 6) && (bitCount == 59))

and the following for a frame reject:

else if ((prevMark == 1) && ((markCount != 6) || (bitCount != 59)))

Here's the updated Arduino WWVB clock data decode sketch.

Clock WWVB Data Decode Frame Reject
/**********************************************************************
* Clock WWVB Data Decode Frame Reject 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

byte prevWwvbInState; // store previous signal level
unsigned int prevEdgeMillis; // store time signal was read

byte 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
unsigned int errorCount = 0; // keep count of frame errors

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

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 = 3;
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 = 2;
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() {
// Display bit values to terminal screen, output delimited data stream with
// colons at mark and new line at frame start. DEBUG ROUTINES
if (bitVal == 3) {
Serial.print("<- Frame Decode\n : ");
}
else if (bitVal == 2) {
Serial.print(" : ");
}
else {
Serial.print(bitVal, DEC);
}
}

/******************************************************************************
* frameAccept()
*
* Accept function for completed frame decode
******************************************************************************/

void frameAccept() {
// future BCD decode here
}

/******************************************************************************
* frameReject()
*
* Reject function for bad frame decode
******************************************************************************/

void frameReject() {
// 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);
}


/*****************************************************************************
* 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
Serial.print("?c0"); // set cursor off
}



And some WWVB data stream showing we now have frame rejects instead of getting intermittent garbage that might accidentally get decoded into garbage input.

 : 10100001 : 000000011 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 10100010 : 000000011 : 000100100 : 100100101 : 00110000 : 1 : 100100011 :
->Scratch<- Bad Data - Marks: 7 Bits: 61 Frame Errors: 01

: 10100011 : 000000011 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 10100100 : 000000011 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 10100101 : 000000011 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 10100110 : 000000011 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 10100111 : 000000011 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 10101000 : 000000011 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 10101001 : 000000011 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 00000000 : 000000100 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 00000001 : 000000100 : 000100100 : 1001001 : 11 : 001100000 : 100100011 :
->Scratch<- Bad Data - Marks: 7 Bits: 61 Frame Errors: 01

: 00000010 : 000000100 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 00000011 : 000000100 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode
: 00000100 : 000000100 : 000100100 : 100100101 : 001100000 : 100100011 : <- Frame Decode


Now, to change that into something a human can read!

2009-05-26

Decoding WWVB Time Data -> Edge Triggered

Well, they say that by making mistakes, you learn what not to do.

The previous attempt at using pulseIn() to help decode WWVB time was successful if all you wanted was a string of bits, but we really do need loop() to function and not be blocked by the interrupt handler going out to a working lunch where it merrily does mostly what we want, but doesn't let us do anything else. Back to the Arduino IDE.

Let's work on getting a string of bits by a slightly different method. First, the detection needs to be edge triggered. This time we want attachInterrupt() to notify us of both the rising and falling edges of our input pulse.

attachInterrupt(0, readLevel, CHANGE); // fire interrupt on edge detected

This calls our interrupt handler which has two functions, read the level on the pin and flash an LED to show receiver activity.

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


At which point the interrupt handler readLevel() drops out of the picture until another edge is detected. Everything now gets handed off to loop() which checks to see if the previous wwvbInState is different from our current wwvbInState and if there has been a change, runs the pulseValue() function.

In pulseValue(), we come to the table with two facts. A change and a high wwvbInState is a leading edge to a pulse, a change and a low wwvbInState is its following edge. The leading edge saves the current time, the following edge subtracts saved time from the current time to find out how long the pulse lasted in milliseconds. From there a few conditional branches tell us if we have noise, a marker, a one or a zero bit. And now for the code:


Clock WWV Edge Triggered

/**********************************************************************
* Clock WWV Edge Triggered 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

byte prevWwvbInState; // store previous signal level
unsigned int prevEdgeMillis; // store time signal was read
byte bitVal; // bit decoded 0, 1 or Mark
byte badBit; // bad bit, noise detected
byte prevMark; // store previous mark bit

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

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
badBit = 0; // set noise counter to zero
if (wwvbInState == 1) { // rising edge
prevEdgeMillis = edgeMillis; // set previous time to current
}
else { // falling edge
int pulseLength = edgeMillis - prevEdgeMillis; // calculate pulse length millis
if (pulseLength < 100) { // less than 100ms, noise pulses
badBit = 1;
}
else if (pulseLength < 400) { // 800ms carrier drop mark
bitVal = 2;
}
else if (pulseLength < 700) { // 500ms carrier drop one
bitVal = 1;
}
else { // 200ms carrier drop zero
bitVal = 0;
}
if (badBit == 0) {
printBitVal();
}
}
}

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

/******************************************************************************
* printBitVal()
*
* Display bit values to terminal screen, output delimited data stream with
* colons at mark and new line at frame start.
******************************************************************************/

void printBitVal() {
if ((bitVal == 2) && (prevMark == 0)) {
Serial.print(" : ");
digitalWrite(ledMarkPin, HIGH);
prevMark = 1;
}
else if ((bitVal == 2) && (prevMark == 1)) {
Serial.print("\nBit Value: ");
Serial.print("| ");
digitalWrite(ledFramePin, HIGH);
prevMark = 0;
}
else {
Serial.print(bitVal, DEC);
digitalWrite(ledMarkPin, LOW);
digitalWrite(ledFramePin, LOW);
if (bitVal == 0) {
digitalWrite(ledBitPin, LOW);
}
if (bitVal == 1) {
digitalWrite(ledBitPin, HIGH);
}
prevMark = 0;
}
}

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

void printTime() {
Serial.print("?x00?y0?f"); // movie cursor to line 1 char 1, clear screen
}

// 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
Serial.print("?c0"); // set cursor off
}



And here's a sample of the data stream coming out of the Arduino to be displayed on the terminal screen:

Bit Value: | 01101000 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 : 
Bit Value: | 01101001 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 10000000 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 10000001 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 10000010 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 10000011 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 10000100 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 10000101 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 10000110 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 10000111 : 000000100 : 01 : 0100100 : 011100101 : 001100000 : 100100011 :
Bit Value: | 101 : 01000 : 000000100 : 00010010 : 1 : 01 :
Bit Value: | 100101 : 001100000 : 1001000 :
Bit Value: | 1 :
Bit Value: | 10001001 : 000000100 : 000100100 : 011100101 : 001100000 : 100100011 :


As you can see, there was once again a bad patch with four minutes of data unreadable. If this is to drive a clock, we don't want the displayed time jumping erratically. Next time we'll see if we can get some simple frame rejection upon the more egregious receive errors.

TTL Serial LCD Display

If this Arduino - CMMR-6P-60 combination is to have useful interaction with the world, it will need a display. To keep the microprocessor unburdened, it would be best to unload driving the display to another microprocessor and communicate with it using TTL (0-5Vdc) serial.

The Modern Device Serial LCD Board fits our specifications pretty well. Connection to the board is +5Vdc, Ground, and whichever pin on our Arduino we use for TXData (hardware Pin 1 - SoftSerial any free pin). The Serial LCD Board interfaces to any HD44780-compatible LCD display and can use either a 16x1 or 8x2 header. You may purchase the board as a kit or assembled.



Serial commands are sent to the board for geometry setup, special characters, clearing the screen and positioning character writes. The command set is simple to use. Setup commands and special character definitions should be followed by a 300ms delay.



And here's a 16 character, 2 line LCD display for our project. These can be found surplus and as long as they're HD44780-compatible, they should work. The Serial LCD Board will run up to a 20x4 display.

2009-05-25

WWVB Receiver Simulator

What do you do on those days when the neighbor fires up his CD ignition fired weedeater and proceeds to hash out any AM signal within hearing distance? And your WWVB receiver sits there flashing its indicator LED like a crazed, randomized strobe? Well, with a second Arduino handy, you build a WWVB simulator and patch its signal in place of the WWVB receiver. So, here's the sketch.

WWVB Receiver Simulator
/**************************************************************************************
* WWVB Receiver Simulator by Capt.Tagon
*
* For those days when you want to work on your code, but the signal is bad, somebody
* fired something off that spurs on 60kHz or you think you just fried your WWVB receiver.
*
* C-Max CMMR-6P-60 TCO Positive Output Signal Emulation
**************************************************************************************/

#define clockOutPin 13 // data out, send this to your second
// Arduino's WWVB Rx input pin

int bitCount = 60; // 60 bits, one per second


// Clock output data, fiddle the zeros and ones to your heart's content,
// but the twos must remain! All bits noted as blank are ALWAYS ZERO.

byte clockData[60] = { // transmitted data 2 Mark, 1 or 0
2, // 0 frame reference Pr -> *** MUST BE 2 ***
0, // 1 minute 40
0, // 2 minute 20
1, // 3 minute 10
0, // 4 blank
0, // 5 minute 8
0, // 6 minute 4
1, // 7 minute 2
0, // 8 minute 1
2, // 9 mark P1 -> *** MUST BE 2 ***
0, // 10 blank
0, // 11 blank
0, // 12 hours 20
0, // 13 hours 10
0, // 14 blank
0, // 15 hours 8
1, // 16 hours 4
0, // 17 hours 2
1, // 18 hours 1
2, // 19 mark P2 -> *** MUST BE 2 ***
0, // 20 blank
0, // 21 blank
0, // 22 day of year 200
1, // 23 day of year 100
0, // 24 blank
0, // 25 day of year 80
0, // 26 day of year 40
1, // 27 day of year 20
1, // 28 day of year 10
2, // 29 mark P3 -> *** MUST BE 2 ***
1, // 30 day of year 8
0, // 31 day of year 4
0, // 32 day of year 2
1, // 33 day of year 1
0, // 34 blank
0, // 35 blank
0, // 36 UTI sign +
0, // 37 UTI sign -
0, // 38 UTI sign +
2, // 39 mark P4 -> *** MUST BE 2 ***
0, // 40 UTI correction 0.8
0, // 41 UTI correction 0.4
0, // 42 UTI correction 0.2
0, // 43 UTI correction 0.1
0, // 44 blank
0, // 45 year 80
0, // 46 year 40
0, // 47 year 20
0, // 48 year 10
2, // 49 mark P5 -> *** MUST BE 2 ***
1, // 50 year 8
0, // 51 year 4
0, // 52 year 2
1, // 53 year 1
0, // 54 blank
0, // 55 leapyear
0, // 56 leapsecond
1, // 57 dst bit 1 set 11 dst, 00 standard
1, // 58 dst bit 2
2, // 59 frame reference P0 -> *** MUST BE 2 ***
};


void setup() {
Serial.begin(9600);
pinMode(clockOutPin, OUTPUT); // set pin digital output
}


void loop(){
for(int i = 0; i < bitCount; i++) {
if (clockData[i] == 2) {
genMark();
}
else if (clockData[i] == 1) {
genOne();
}
else {
genZero();
}
}
}

void genMark() { //generate 800ms Mark
digitalWrite(clockOutPin, HIGH);
delay(200);
digitalWrite(clockOutPin, LOW);
delay(800);
}

void genZero() { // generate 200ms Zero
digitalWrite(clockOutPin, HIGH);
delay(800);
digitalWrite(clockOutPin, LOW);
delay(200);
}

void genOne() { // generate 500ms One
digitalWrite(clockOutPin, HIGH);
delay(500);
digitalWrite(clockOutPin, LOW);
delay(500);
}

Decoding WWVB Time Data -> Data Stream

Now that we've got a 5V TTL signal being output on the TCO pin, we can start seeing if the Arduino can make any sense of it. Somehow, the Arduino needs to be instructed to look for an edge on a square wave signal, and preferrably for our convenience, a rising edge. It also would be nice if the microprocessor were free to do something else while waiting around.

Our first WWVB sketch will make use of an external interrupt using the attachInterrupt() function. Digital Pin 2 is used by Interrupt 0 so we'll use this for our signal input pin.

attachInterrupt(0, readPulse, RISING); // attach interupt handler to INT0

This will trigger an interrupt handler when the rising edge of the signal is detected. Our interrupt handler is the readPulse() function. How this is supposed to work; the loop() sits there running its operations, and somewhere during these operations, the voltage rises on Pin 2. The ISR takes over and says, "Steady on, we have an important message, wait while I handle it, I need to measure the length of this pulse." readPulse() uses pulseIn() to time how long the pulse remains high on Pin 2 in microseconds.

pulseWidth = pulseIn(wwvbPin, HIGH); // record duration of pulse

While this might work just fine for intermittent pulses, the Arduino seems to go into a wait state while timing the pulse duration, not a very efficient use of clock cycles. It actually seems to block loop() from ever executing. A better method would be an interrupt driven routine that would detect a rising edge and the time it occured, then detect a falling edge and calculate the difference to report how long the pulse was. Quite a few clock cycles would be freed on the microprocessor for other things. Another issue for another day (plus that's already been done in the DCF-77 code).

Now that our sketch is calculating pulse lengths, a series of matches can figure out whether we've got noise, ones, zeros or marks. View the output on the Arduino IDE serial terminal screen. On to the code!

Clock WWVB Data Decode - External Interrupt - pulseIn()
/******************************************************************************
* Clock WWVB Data Decode - External Interrupt - pulseIn() by capt.tagon
*
* WWVB receiver input on digital pin 2
******************************************************************************/

#define wwvbPin 2 // WWVB receiver data input signal pin
#define ledMarkPin 7 // Data received mark inicator pin
#define ledFramePin 6 // Data received frame indicator pin
#define ledBitPin 5 // LED data decoded indicator pin

//#define DEBUG

/* received pulse width 200ms = 1, 500ms = 0, 800ms = markers and frame start */
/* volatile variables, changed by interrupt handler */
volatile unsigned long pulseWidth;


int bitVal = 0; // decoded bit
int badBit = 0; // noise decoded
int bitReady = 0; // bit waiting
int prevMark = 0; // store previous mark bit

void setup() {
pinMode(wwvbPin, INPUT);
pinMode(ledMarkPin, OUTPUT);
pinMode(ledFramePin, OUTPUT);
pinMode(ledBitPin, OUTPUT);
attachInterrupt(0, readPulse, RISING); // attach interupt handler to INT0
Serial.begin(9600);
}

void loop() {
Serial.println("NOP"); // test for interrupt handler release
delay(200);
}

/***********************************************************************
* readPulse()
*
* Pin 2 INT0 Interrupt Handler Reads pin state
************************************************************************/

void readPulse() {
pulseWidth = pulseIn(wwvbPin, HIGH); // record duration of pulse

// Once started, the interrupt handler never seems to release any time
// to loop() to run anything. Prove it by commenting out the following
// line. You should see NOP printed to the terminal. NOP only prints
// on reset. Worse than polling to decode, pulseIn() IS only useful for
// intermittent pulse duration measurement.

pulseValue();
}


/******************************************************************
* pulseValue()
*
* determine pulse width 200ms = 0, 500ms = 1, 800ms = mark
* output delimited data stream with colons at mark and new line at
* at frame start.
******************************************************************/

void pulseValue() {

badBit = 0;

if (pulseWidth > 500000) { // carrier dropout - no signal
bitVal = 5;
badBit = 1;
}
else if (pulseWidth > 310000) { // carrier 500 ms drop bit = 0
bitVal = 0;
bitReady = 1;
}
else if (pulseWidth > 135000) { // carrier 200 ms drop bit = 1
bitVal = 1;
bitReady = 1;
}
else if (pulseWidth > 115000) { // carrier 800 ms drop 1 = 10 sec, 2 = frame start
bitVal = 2;
bitReady = 1;
}
else { // spikes/noise
bitVal = 7;
badBit = 1;
}

#ifdef DEBUG
if (bitVal != 7) {
Serial.print(bitVal, DEC);
Serial.print(" : ");
Serial.print(badBit, DEC);
Serial.print(" : ");
Serial.print(bitReady, DEC);
Serial.print(" :");
Serial.println(pulseWidth, DEC);
}
#endif
if (bitVal != 7) {
printBitVal();
}
}

void printBitVal() {
if ((bitVal == 2) && (prevMark == 0)) {
Serial.print(" : ");
digitalWrite(ledMarkPin, HIGH);
prevMark = 1;
}
else if ((bitVal == 2) && (prevMark == 1)) {
Serial.print("\nBit Value: ");
Serial.print("| ");
digitalWrite(ledFramePin, HIGH);
prevMark = 0;
}
else {
Serial.print(bitVal);
digitalWrite(ledMarkPin, LOW);
digitalWrite(ledFramePin, LOW);
if (bitVal == 0) {
digitalWrite(ledBitPin, LOW);
}
if (bitVal == 1) {
digitalWrite(ledBitPin, HIGH);
}
prevMark = 0;
}
bitReady = 0;
}


And here's some WWVB data output. Time is streaming in, but not in a readily readable format, eh? Also notice that three of the lines had a noise problem.

Bit Value: | 01100111 : 000000100 : 000100100 : 011000101 : 001100000 : 100100011 : 
Bit Value: | 01101000 : 000000100 : 000100100 : 011000101 : 001100000 : 100100011 :
Bit Value: | 01101001 : 000000100 : 000100100 : 011000101 : 001100000 : 100100011 :
Bit Value: | 10000000 : 0000001001000100100 : 011000101 : 001100000 : 100100011 :
Bit Value: | 10000001 : 000000100 : 000100100 : 011000101 : 001100000 : 100100011 :
Bit Value: | 10000010 : 000000100 : 000100100 : 011000101 : 001100000 : 100100011 :
Bit Value: | 10000011 : 000000100 : 000100100 : 011000101 : 0001000001100000011 :
Bit Value: | 10000100 : 000000100 : 000100100 : 0110001011001100000 : 100000011 :
Bit Value: | 10000101 : 000000100 : 000100100 : 011000101 : 001100000 : 100100011 :
Bit Value: | 10000110 : 000000100 : 000100100 : 011000101 : 001100000 : 100100011 :

C-MAX CMMR-6P-60 WWVB 60kHz Receiver


Back in the day, Heathkit produced "The Most Accurate Clock". It was an HF receiver that tuned to WWV and WWVH on 5, 10 and 15MHz and updated its clock from the decoded time signal.

Currently most people are familiar with "Atomic Clocks" which pick up another NIST time signal, WWVB on 60kHz. Various websites detail how to extract the receiver boards and antennas from consumer equipment to recover these receivers. If you have an old one sitting around that you're willing to sacrifice, or can pick up a used one for less than $10, this may be a way to go.


C-MAX
makes the CMMR-6P-60 for receiving the US NIST WWVB time signal. Here is the datasheet.

I got my CMMR-6P-60 from DigiKey for $10.70. They list it as P/N 561-1014-ND. It comes with an attached tuned 60mm loopstick antenna. If you are any distance from Colorado, or are having problems with noise, you might want to purchase the 100mm 60kHz tuned loopstick antenna for an extra $2.00. Its part number is 561-1001-ND. Just unsolder the 60mm loopstick and solder the 100mm loopstick in its place.



Some considerations with using this receiver. It's a receiver. Get it off the work bench away from RF hash and near a window. The antenna is a directional dipole antenna. Turn the loopstick broadside to a direction facing Boulder, Colorado. RF grounding and shielding may be necessary. Currently, mine runs with the CMMR-6P ground attached to the grounded shell on my computer's USB port. Otherwise the TCO output goes high and remains there. The VLF 60kHz signal seems to be best around sunrise and sunset.

The main thing we're interested in at the moment is powering up the board. We'll need to provide some power filtration, tie PON to ground to power up the receiver, get a signal out, and have some sort of indicator we can watch to see if something's happening. The following schematic will allow you to apply basic power and watch a blinking light.



One thing to remember about this signal, it's a slow one, your bit is one second long. Any fast flickering or constant on indicates interference of some sort. The LED should flash once per second. The receiver outputs two signals. TCO is the positive following of the transmitted data and TCON is inverted. We hook the LED to the TCON because it proportionately spends more of its time off which means the receiver can be battery powered (less current drain) while searching for a noise free environment.

Solid heartbeat of 60 per minute means that the next step can be tackled, turning the TCO output into time, or at least a usable data stream. There are a few libraries and Arduino sketches out there for decoding DCF-77. The DCF-77 time signal has some nice amenities and data layout. We aren't so lucky. None of it will work with the WWVB format (see Chapter 2).

The WWVB signal is a continously transmitted 60kHz carrier that drops carrier power to produce the pulses we can decode into 0, 1 and marker data. A 200ms power drop indicates 1, 500ms power drop indicates 0, 800ms drop indicates 10 second marker (6ppm), two 800ms drops in a row indicate start of the next data frame (previous P0 marker plus Pr marker). A visual representation of the pulse train can be found here.

2009-05-24

Array of Instances - Indexed Lists With Added Power

Let's see what's in the intensity filing cabinet.

Hmm, drawer 0 labeled Red contains 127, drawer 1 labeled Green contains 192 and drawer 2 labeled Blue contains 50. Send the respective value as a PWM output to each terminal on the RGB LED.

Indexed lists, commonly known as arrays are a powerful tool for arranging groups of variables. We've seen them used for organizing lists of data such as pin assignments, multiple sensor inputs and character strings. Can we do the same for objects?

Here's a sample sketch showing how to set up an "Array of Instances" and then use a for() loop to do something with that array.

We will be using the HAL LED Library to create the instances in the array, so include the library:

#include <LED.h>   // include HAL LED class

and then create the array:

//create array of LED instances
LED led[7] = {   // refer to LEDs as led[0] thru led[6]
LED(6),
LED(7),
LED(8),
LED(9),
LED(10),
LED(11),
LED(12)
};


and here's the full sketch.

Knight Rider 4
/*****************************************************************************
* Knight Rider 4
*
* @Sketch by Capt.Tagon showing use of LED class
* and an array of instances thereof.
*
* Using LED abstraction library by Alexander Brevig
* http://www.arduino.cc/playground/Code/LED
*
* @description
* Demonstrates the functionality of the LED class
*
* attributes
* .getState() returns current LED state
*
* digital methods
* .on() turns LED on
* .off() turns LED off
* .toggle() changes current LED state
*
* analog methods -> use on ATmega168 Arudino pins 3, 5, 6, 9, 10, and 11
* .setValue() sets PWM 0-255 value
* .fadeIn(millis) on PWM pin, fades in over millis time
* .fadeOut(millis) on PWM pin, fades out over millies time
*
* create array of LED instances and play Knight Rider with them
******************************************************************************/

#include <LED.h> // include HAL LED class

//create array of LED instances
LED led[7] = { // refer to LEDs as led[0] thru led[6]
LED(6),
LED(7),
LED(8),
LED(9),
LED(10),
LED(11),
LED(12)
};

byte numLeds = 7; // number of LEDs for counter
byte count; // interate through LED array
int timer = 100; // delay between steps

void setup(){
Serial.begin(9600); // Serial debugging output
}

void loop(){
for (count = 0; count < (numLeds - 1); count ++) {
Serial.println(count, DEC); // print count up
led[count].on();
delay(timer);
led[count].off();
delay(timer);
}
for (count = (numLeds - 1); count > 0; count --) {
Serial.println(count, DEC); // print count down
led[count].on();
delay(timer);
led[count].off();
delay(timer);
}
}

HAL - Hardware Abstraction Library, or "Yes, I can do that, Dave."

Cheesy title aside, Hardware Abstraction Libraries are an attempt to bring the full power of Object Oriented Programming to bear on hardware interaction. In the Arduino world, the simplest example is HAL Class LED. Download the LED.zip file, and extract the LED folder to your Arduino/hardware/libraries folder. There are several other HAL classes available.

You can dispense with a lot of coding and pare it down to the basics.

First, include the library so you have access to the LED class.

#include<LED.h>

Declare an instance of the class (object) for the pin that controls the LED.

LED led0 = LED(6);

and then use attributes (data) to ask for information about the instance and methods (behaviors) to control the instance.

Class Attributes
  • .getState() returns current LED state
Class Methods
  • .on() turns LED on
  • .off() turns LED off
  • .toggle() changes current LED state
  • .blink() halt for millis and blink LED x times
  • .setValue() sets PWM 0-255 value
  • .fadeIn(millis) on PWM pin, fades in over millis time
  • .fadeOut(millis) on PWM pin, fades out over millis time


The sketch LEDLib demonstrates use of various LED class methods in controlling LEDs.

LEDLib
/***************************************************************
* LEDLib - Demonstrate HAL library for LED class by capt.tagon
*
* HAL Class LED library by Alexander Brevig (alphabeta)
* http://www.arduino.cc/playground/Code/LED
*
* Class Attributes
* .getState() returns current LED state
*
* Class Methods
* .on() turns LED on
* .off() turns LED off
* .toggle() changes current LED state
* .blink() halt for millis and blink LED x times
* .setValue() sets PWM 0-255 value
* .fadeIn(millis) on PWM pin, fades in over millis time
* .fadeOut(millis) on PWM pin, fades out over millis time
****************************************************************/

#include <LED.h>

//create instance of LED for each pin
LED led0 = LED(6);
LED led1 = LED(8);
LED led2 = LED(9);
LED led3 = LED(10);
LED led4 = LED(11);

void setup(){
led4.setValue(127); // LED setValue method - set PWM 50%
}

void loop(){
led0.blink(500, 10); // LED blink method -- signal start
ledDemo(); // run main demo
}

void ledDemo() {
for (int i = 0; i<6; i++) { // five iterations and then blink led0
led1.on(); // LED on method
delay(250);
led1.off(); // LED off method
delay(250);
led2.toggle(); // LED toggle method - flip LED state
led3.fadeIn(750); // LED fadeIn method - PWM to full on
led3.fadeOut(1000); // LED fadeOut method - PWM to full off
}
}

Ye Olde Output Indicator - LED Array

Somewhere along the line after pushing your current limiting resistors into the solderless breadboard for about the five millionth time, and then reaching for the mini-needlenose pliers to straighten the leads for an accumulated about half as many times...

You get to thinking, "there's got to be a better way". Yeah, sure 1k resistors are cheap, but that's not the point.

So in looking over at the parts bin, I noticed a hand full of green and red 3mm LEDs from some forgotten project from a decade ago, the typical yellow of some resistor network chips on a failed piece of Cisco routing equipment and had a "Eureka!" moment. These items, and a 1 inch square proto board I'd just purchased plus a 90 degree header pin strip could relieve me from the bent lead annoyance and free up time to mess with trying to burn out expensive sensors while seeing if the programming logic comes anywhere near driving the eventual motor controller board correctly.

The proto board had nine holes along one edge, so we can get a whole byte's worth of flashing digital goodness. And here's the result.


Equipment List:
  • RN1 - 1k isolated 16 pin DIP resistor network
  • LED1-8 8 red or green 3mm LEDs
  • 90 degree header pins, snap off 9
  • 1 inch square MExplorer proto board
The array is wired for common cathode, pin high drives the LED on. Now getting one or more LEDs into operation is just a simple push of the array into the breadboard, a jumper wire for ground, one for each LED to activate and you're up and running.

I've been using these with the BoArduino, so I make them in pairs and on one, swap the end the common cathode pin is on. This allows the LEDs to face in the same direction when the boards are plugged in on the digital pins.

And here's a schematic.

2009-05-23

Autocalibrate and Analog Sensors

Most analog sensors like LDRs (Light Dependent Resistor) and FSRs (Force Sensitive Resistor) take a little fiddling to get useful input.

Your Arduino has a 10 bit ADC (Analog to Digital Converter) on its analog inputs (0-1023 steps) which accepts a voltage swing of 0 to +5Vdc. This has to translate to an 8 bit PWM (0-255 steps) signal on one of the PWM capable digital output pins.

You need to create a voltage divider using the variable resistor and a fixed resistor, that will get as close to the 5 volt swing as possible. LDRs and FSRs have a nominal value, and not much accuracy. Components in the same batch can vary in their at rest resistance and in their change in resistance to equal inputs.

The easiest method of calibration would probably be to just give up and put a potentiometer wired as a rheostat on each sensor, tweak and be done. Of course that adds to the expense of the circuit, so we probably want to use a fixed resistor. The formula to figure this is fairly simple for a two resistor circuit:
Vout = R2/(R1+R2)*Vin

Daycounter Inc. voltage divider calculator

For our purposes, we can view this voltage divider as being unloaded as it is feeding into a high impedance load (Arduino ADC pin). Once you get the fixed resistor close enough to give you a usable voltage swing, why not let the microcontroller do a little bit of fine tuning?

We can read the sensor value on an analog pin and use it to vary the output on a PWM pin easily enough, just take the input reading, divide by 4 (10 bit to 8 bit) and set the analogWrite() to use that value.

In the following sketch, we will add a variable to store a maximum reading, a variable to store a minumum reading and then use the map() function to convert the recorded input range into a useful output range (full 0 to 255).

Just in case we get something odd, we can also use the constrain() function to keep the output between 0 and 255 so we don't get odd results (256 wraps to 0).

Added to the mix is the idea of adding a threshold function which smooths out the response by ignoring changes below a chosen value.

Brightness Sensor Autocalibrate
/**********************************************************************
* Brightness Sensor Autocalibrate by capt.tagon
*
* Modification on Example 06B in "Getting Started with Arduino"
* by Massimo Banzi (co-founder of Arduino
*
* Set the brightness of the LED to a value determined by an
* analog sensor on specified sensor pin.
* State is recorded and compared to threshold value.
* Auto calibrate from sensor input to get full output swing (0-255)
***********************************************************************/

#define ledPin 11 // the pin for the LED
#define sensorPin 0 // the pin for the sensor input

int sensorVal = 0; // variable for reading sensor value
int mappedSensorVal = 0; // variable for holding remapped sensor value
int prevSensorVal = 0; // variable for holding previous sensor value
int sensorMin = 1023; // variable for holding maximum sensor value
int sensorMax = 0; // variable for holding minimum sensor value
int THRESHOLD = 2; // sensor change threshold

void setup() {
pinMode(ledPin, OUTPUT); // LED is as an OUTPUT
Serial.begin(9600); // Start serial output
}

void loop() {

sensorVal = analogRead(sensorPin); // read value from the sensor

if (sensorVal > sensorMax) { // record maximum sensor value read
sensorMax = sensorVal;
}

if (sensorVal < sensorMin) { // record minimum sensor value read
sensorMin = sensorVal;
}

/* map 10-bit ADC value, potentially 0-1023 to 8-bit pwm output 0-255
adjusted to actual minimum and maximum values read by ADC */
mappedSensorVal = map(sensorVal, sensorMin, sensorMax, 0, 255);

/* compare current reading with previous and only change if the
threshold value is exceeded */
if (abs(mappedSensorVal - prevSensorVal) >= THRESHOLD) {
Serial.print("Level: "); // serial debug output
Serial.println(mappedSensorVal);
mappedSensorVal = constrain(mappedSensorVal, 0, 255); // limit to 0-255
analogWrite(ledPin, mappedSensorVal); // use PWM to set LED brightness
prevSensorVal = mappedSensorVal; // save mapped sensor value to
} // compare to threshold
}