2009-06-27

Temporary Connections, Just Spring Probe It!

One of the lures of using an Arduino programmed Atmel Microprocessor is that at the slightest whim, you can attach a two wire serial cable connection to the RX/TX pins and from the Arduino IDE, reprogram the chip for its upgraded function within the circuit designed around it.

A BoArduino has a 6 pin header connector for the FTDI TTL-232R USB to 5.0v TTL serial cable. This takes up a bit of real estate, plus it's 3 more pins than we really need. In our circuit design around the Atmel microprocessor, a three pin header with connections for RX Data, TX Data and signal ground is all that's really necessary.

What if you wanted to eliminate the need for a header all together? Enter stage left, the spring probe. For obvious reasons, Everett Charles Technologies (ECT) trademarked their contact test probes under the name "Pogo Pin" which has also become somewhat a generic term for these handy devices.

The brass body is just the right size to slip through the holes in any 0.040" dia. drilled board to be soldered into place for making circuit test adapters, programming jigs, or more permanent connections between stacked proto-boards. The spring-loaded contact head is 0.055 dia. and shaped so it will center in 0.040 drilled holes, making contact with the copper trace and gets best contact when used in plated through-holes.

Two samples below out of the many head configurations used in spring probes.

The top is a chisel tip for probing plated through-holes.

The bottom is a ball tip for use on flat pads or holes.
Another tip design used for pads is the "Potato Masher" tip with a crosscut diamond raised contact surface. Cup tipped probes can be used for automated test jig contact with header pins, the cup quickly centering on the pointed top of the header pin.

Example of creating a test fixture using spring probes.

2009-06-23

Making Connections the IDC Way

One of the most annoying parts of prototyping can be connecting everything up. With the solderless breadboard, everyone starts off with "ye bundle of telephone wire" and gets pretty adept at stripping back the ends and board stuffing. After a few uses, the ends bend up and soon thereafter get tossed in the wastebasket.

One of the nicer pieces of equipment for wire-up are the male header pin cables currently available for use with 0.100 spaced boards. These give you a flexible wire with a solid header pin just made for board insertion and fit well side by side on the board.

That's all well and good for two or three wires side by side, but what if you want to run a three wire cable over to a sensor three feet away, or want to jump eight digital ports from an Arduino 2009 over to a solderless board because you haven't quite yet committed to stuffing one of those really nice proto-shields full of components for a potentially flaky circuit?

You could get some ribbon cable, strip back the ends, get out the needle-nose pliers and solder them down to some header pins. That works for a while, but if you aren't careful, somewhere along the way, one of the wires breaks after too many flexures, plus there isn't much room for getting a grip on the header to pull it out. The other method is to use crimp female header sockets, but then we're back to stripping wire and I've had to deal with rotten, cheap crimpers that almost but not quite crimp about one in every four sockets. Enter a connection method used in the avionics industry...



This is known as an IDC (Insulation Displacement Connector). This particular version is made for 0.100" single row headers and 22 gauge wire. It's a little overkill for our purposes. The ribbon cable we're working with is typically 28 gauge. Let's have a look at the Amp MTA 100 series IDC.



Here we have a 3 pin connector. The wires slide into the top of the connector with the ends flush against the vertical wall at the back of the connector. For production purposes, this female connector has a mating male connector that solders into the board. It has a clip locking action, but that is superfluous for prototyping.



The wires are inserted into the connector with a punchdown tool.
And visually, here's the process: 1) Cut the ribbon cable to length. 2) Separate the wires. 3) Insert first wire with end touching wall. 4) Punch down with tool.









And here's the finished connector and a finished 18" 3-wire cable for about $1.40. Female/female servo style cables are readily available for about $3.00, but where this shines is when you want four or more wires in your cable.





The final bit for making this work with your solderless breadboard is to get out your snappable double-side header pins, snap off the appropriate number, push the long end into the socket and you have an instant male/male cable that will insert into the Arduino socket and your solderless breadboard.




2009-06-21

Supressing RFI or Why won't my clock calibrate?

One of the primary things to watch out for in designing electronic circuits is noise suppression. Our microprocessors thrive on square waves, which can generate overwhelming broadband interference.

Any radio DXer can fill you in to great depth about RFI and its impact on their hobby. From the neighbor's electric fence to keep his horses in check to the lowly capacitive switched touch lamp or light dimmer, badly designed and/or failing circuits are his bane. As an example, here's what you need to do to the average wall wart to render it into a smooth, noise free power source for an active antenna.

Early, relatively unshielded computers are a disaster when introduced to a radio room.

In our WWVB clock, we are dealing with several noise sources that will likewise degrade the operation of our 60 kHz WWVB radio receiver. The Arduino has a 16 MHz clocked microprocessor, the LCD display serial interface has its own PIC microprocessor. Under these conditions, good circuit design includes establishment of a ground plane, and capacitors to bypass any unwanted RF noise to ground at the source. For our purposes, we need a 0.1uF capacitor on the microprocessor end of our CMMR-6P WWVB receiver's power supply cable, and a 0.1uF capacitor on the power supply to the LCD display.

The WWVB receiver board has already been supplied with power filtration on the other end of the cable.

Intersil Application Note: "Choosing and Using Bypass Capacitors" is something to think about the next time you are getting oddball results.

2009-06-20

The Arduino - CMMR-6P-60 "Almost Accurate Clock"

Well, the machining, shimming and tuning took hold and we've got something that will output UTC, 4 US time zones and the date. The temperature readout is Celsius, but with a change of a chip, and a little code, we could have Fahrenheit.

Yep, It's Alive!



Major code modifications were as follows:

* Decode WWVB data stream, Short impulse noise filtering, Drop bad frames,
* Display: Signal Quality Indicator, Month & Day from Day of Year data,
* Time Zone & DST Indicator for Local Time Display. Extend clock function
* to increment day and year for midnight rollover and year end rollover.
*
* Choose UTC/Local Time, Choose between four US Time Zones for local time
*
* Note that UTC time display and Time Zones are set with DIP switches, flip settings
* and hit reset button. Polling switches to dynamically change UTC/LocalTime/TZ messes
* up the receiver decoding by introducing oddball delays that make it mistime the falling edge.
* Dip-One 0=Local, Time 1=UTC | Dip-Two&Three 00=E, 01=C, 10=M, 11=P
*
* Frame Error Indicator - serves as signal indicator - Arduino serves as clock
* during the noisy parts of the day, as it starts picking up WWVB frames, it syncs
* to NIST time.
* 1. Continuous full block indicates time as accurate as propagation delay will allow.
* 2. Block and half block alternating. Signal is degrading, sync update when block shows.
* 3. Half block, 5 or less sequential frame errors, low bar more than 5 frame errors.
* 4. Low bar continuous, poor WWVB signal, no sync. Running on microprocessor clock only.
* Your time will only be as accurate as your Arduino crystal allows. Date functions
* will also rollover at midnight and year end.

I probably need to work over the date functions to make sure all the months function. I used the WWVB Simulator to check a few month endings, but if you don't check all iterations, you get bit by assuming it just works.

So, here it is, The Arduino - CMMR-6P-60 "Almost Accurate Clock"


/*************************************************************************************************
* Arduino WWVB C-Max CMMR-6P-A2 clock v1.0 using the CMMR-6P-60 Eval Kit
*
* With apologies to Heathkit, we miss you! --> "The Almost Accurate Clock"
*
* Get the long tuned ferrite loopstick antenna and replace what's soldered to the board.
*
* Remote mount the receiver unit near a window if possible and well away from other electronic
* equipment for best results.
*
* Continental US Summer longwave propagation will give best signal at sunrise and sunset.
* Continental US Winter longwave propagation may give nearly 24 hr signal dependent on
* your location and local interference.
*
* capt.tagon's contributions
*
* Decode WWVB data stream, Short impulse noise filtering, Drop bad frames,
* Display: Signal Quality Indicator, Month & Day from Day of Year data,
* Time Zone & DST Indicator for Local Time Display. Extend clock function
* to increment day and year for midnight rollover and year end rollover.
*
* Choose UTC/Local Time, Choose between four US Time Zones for local time
*
* Note that UTC time display and Time Zones are set with DIP switches, flip settings
* and hit reset button. Polling switches to dynamically change UTC/LocalTime/TZ messes
* up the receiver decoding by introducing oddball delays that make it mistime the falling edge.
* Dip-One 0=Local, Time 1=UTC | Dip-Two&Three 00=E, 01=C, 10=M, 11=P
*
* Frame Error Indicator - serves as signal indicator - Arduino serves as clock
* during the noisy parts of the day, as it starts picking up WWVB frames, it syncs
* to NIST time.
* 1. Continuous full block indicates time as accurate as propagation delay will allow.
* 2. Block and half block alternating. Signal is degrading, sync update when block shows.
* 3. Half block, 5 or less sequential frame errors, low bar more than 5 frame errors.
* 4. Low bar continuous, poor WWVB signal, no sync. Running on microprocessor clock only.
* Your time will only be as accurate as your Arduino crystal allows. Date functions
* will also rollover at midnight and year end.
*
* Contributed code that makes this all happen. Thank you for the informative websites,
* time spent and sharing your code. I know how much I spent re-kajiggering for WWVB whose
* data stream is a testament to cold war technology .
*
* Captain http://www.captain.at/electronic-atmega-dcf77.php
* DCF77 time reception and decoding using the ATMega 16
* Mathias Dalheimer http://gonium.net/md/2006/11/05/arduino-dcf77-radio-clock-receiver/
* Interrupt driven clock and interrupt driven edge detection for DCF77
* Rudi Niemeijer http://www.rudiniemeijer.nl/wordpress/?p=516
* Amendments to DCF77 code, time display and addition of temperature functions
* Peter H Anderson http://phanderson.com/lcd106/lcd107.html
* LCD Display functions using LCD117 Serial LCD driver
*************************************************************************************************/

#include // sprintf 2k penalty, abandon for Serial.print() if memory low

//Inputs
#define wwvbRxPin 2 // WWVB receiver digital input
#define utcSwitchPin 8 // Localtime/UTC selector switch digital input
#define tz1SwitchPin 9 // TZ selector switch digital input
#define tz2SwitchPin 10 // TZ selector switch digital input
#define tempSensorPin 5 // LM35 temperature sensor analog input
//Outputs
#define ledMarkPin 7 // LED mark indicator digital output
#define ledFramePin 6 // LED frame indicator digitaloutput
#define ledBitPin 5 // LED bit value indicator digital output
#define ledRxPin 4 // LED indicator digital output
//Constants
#define WWVB_noise_millis 100 // Number of milliseconds before we assume noise (100ms)
#define WWVB_mark_millis 400 // Number of milliseconds before we assume a mark (200ms)
#define WWVB_split_millis 700 // Number of milliseconds before we assume a logic 1 (500ms=1,800ms=0)

/* Definitions for the timer interrupt 2 handler
* The Arduino runs at 16 Mhz, we use a prescaler of 64 -> We need to
* initialize the counter with 6. This way, we have 1000 interrupts per second.
* We use tickCounter to count the interrupts.
*/
#define INIT_TIMER_COUNT 6
#define RESET_TIMER2 TCNT2 = INIT_TIMER_COUNT
int tickCounter = 0;

// Time variables
volatile byte ss; // seconds
volatile byte mm; // minutes
volatile byte hh; // UTC hours
volatile byte lhh; // local time hours
volatile byte day; // day
volatile word doy; // day of year
volatile byte mon; // month
volatile word year; // year
volatile byte dst; // daylight saving time flag
volatile byte lyr; // leap year flag
volatile byte prevday; // previous day for local time
volatile byte prevmon; // previous month for local time
volatile byte dayxing; // 00:00 UTC day change to Local

// WWVB receiver signal processing variables
volatile byte wwvbSignalState = 0; // store wwvb receiver state
byte previousSignalState; // save previous receiver signal state
int previousFlankTime; // detected signal edge time
int bufferPosition; // buffer position counter
unsigned long long wwvbRxBuffer; // 64 bit data buffer to hold 60 bits decoded from WWVB

/* WWVB time format struct - acts as an overlay on wwvbRxBuffer to extract time/date data.
* All this points to a 64 bit buffer wwvbRxBuffer that the bits get inserted into as the
* incoming data stream is decoded.
*/
struct wwvbBuffer {
unsigned long long U12 :4; // no value, empty four bits only 60 of 64 bits used
unsigned long long Frame :2; // framing
unsigned long long Dst :2; // dst flags
unsigned long long Leapsec :1; // leapsecond
unsigned long long Leapyear :1; // leapyear
unsigned long long U11 :1; // no value
unsigned long long YearOne :4; // year (5 -> 2005)
unsigned long long U10 :1; // no value
unsigned long long YearTen :4; // year (5 -> 2005)
unsigned long long U09 :1; // no value
unsigned long long OffVal :4; // offset value
unsigned long long U08 :1; // no value
unsigned long long OffSign :3; // offset sign
unsigned long long U07 :2; // no value
unsigned long long DayOne :4; // day ones
unsigned long long U06 :1; // no value
unsigned long long DayTen :4; // day tens
unsigned long long U05 :1; // no value
unsigned long long DayHun :2; // day hundreds
unsigned long long U04 :3; // no value
unsigned long long HourOne :4; // hours ones
unsigned long long U03 :1; // no value
unsigned long long HourTen :2; // hours tens
unsigned long long U02 :3; // no value
unsigned long long MinOne :4; // minutes ones
unsigned long long U01 :1; // no value
unsigned long long MinTen :3; // minutes tens
};

// Decode variables for flags, counters and state
boolean signalNoise = 0; // 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
byte frameError = 1; // set for frame reject
word errorCount = 0; // keep count of frame errors

// Time Zone definition array, identifier & offset
byte TZ[4][2] = {
{'E', 5}, // UTC -5 Eastern
{'C', 6}, // UTC -6 Central
{'M', 7}, // UTC -7 Mountain
{'P', 8} // UTC -8 Pacific
};

// Selector switch input variables UTC/Local & Time Zone Select
boolean displayUTC = 0; // change to 1 to display UTC
byte selectTZ = 3; // 0=ET, 1=CT, 2=MT, 3=PT

// End of Month - to calculate Month and Day from Day of Year
int eomYear[14][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
{366,367} // overflow
};


byte previousSecond; // store state to trip updates when second changes

boolean clockStarted = 0; // valid time detected, clock started, clear screen

float calculatedTemperature = 0; // calculated temperature from sensor

/**********************************************************************************
* Arduino setup() routines to initialize clock
**********************************************************************************/

void setup(void) {
Serial.begin(9600); // Serial communications for LCD display
// UTC display inputs
pinMode(utcSwitchPin, INPUT); // set pin mode input
pinMode(tz1SwitchPin, INPUT); // set pin mode input
pinMode(tz2SwitchPin, INPUT); // set pin mode input
lcdInit(); // set lcd display mode, clear screen
utcSwitchCheck(); // dip switch read during restart - display LocalTime/UTC
tzSwitchCheck(); // dip switch read during restart - choose timezone to display
getTemperature(); // read temperature from sensor
wwvbInit(); // initialize clock, ISR for time correction
clearLCD(); // clear display for output
}

/**********************************************************************************
* Main loop()
**********************************************************************************/

void loop(void) {
if (ss != previousSecond) { // upon seconds change
if ((ss % 5) == 0) { // every five seconds
getTemperature(); // read temperature sensor and compute temp degrees Celsus
}
#ifndef DEBUG_DATASTRUC
serialDumpTime(); // print date, time and temp to LCD
#endif
previousSecond = ss; // store previous second
}
if (wwvbSignalState != previousSignalState) { // upon WWVB receiver signal change
scanSignal(); // decode receiver data
previousSignalState = wwvbSignalState; // store previous receiver signal state
}
}

/***************************************************************************************
* wwvbInit()
*
* Initialize variables, set clock to zero, set pin modes, configure Timer2,
* and attach interrupt for incoming WWVB data.
***************************************************************************************/

void wwvbInit() {
previousSignalState = 0; // clear variables
previousFlankTime = 0;
bufferPosition = 63; // set receive buffer position for decrement
wwvbRxBuffer = 0; // empty receive buffer

ss=mm=hh=day=mon=year=0; // initalize clock time to Zero

// WWWVB Receiver input and indicators
pinMode(wwvbRxPin, INPUT); // set WWVB Rx pin input
pinMode(ledRxPin, OUTPUT); // set LED indicator outputs
pinMode(ledBitPin, OUTPUT);
pinMode(ledMarkPin, OUTPUT);
pinMode(ledFramePin, OUTPUT);

// Timer2 Settings: Timer Prescaler /64,
TCCR2B |= (1<<CS22); // turn on CS22 bit
TCCR2B &= ~((1<<CS21) | (1<<CS20)); // turn off CS21 and CS20 bits
// Use normal mode
TCCR2A &= ~((1<<WGM21) | (1<<WGM20)); // turn off WGM21 and WGM20 bits
TCCR2B &= ~(1<<WGM22); // turn off WGM22
// Use internal clock - external clock not used in Arduino
ASSR |= (0<<AS2);
TIMSK2 |= (1<<TOIE2) | (0<<OCIE2A); //Timer2 Overflow Interrupt Enable
RESET_TIMER2;

attachInterrupt(0, int0handler, CHANGE); // attach interrupt handler and fire on signal change
}

/***********************************************************************************
* Clock counter - The interrupt routine for counting seconds - increment hh:mm:ss.
***********************************************************************************/

ISR(TIMER2_OVF_vect) {
RESET_TIMER2;
tickCounter += 1; // increment second
if (tickCounter == 1000) {
ss++;
if (ss == 60) { // increment minute
ss = 0;
mm++;
if (mm == 60) { // increment hour
mm = 0;
hh++;
if (hh == 24) { // increment day
hh = 0;
doy++;
if (doy == (366 + lyr)) { // incr year
doy = 1;
year++;
}
}
}
}
tickCounter = 0;
}
}

/***********************************************************************
* int0handler()
*
* Interrupthandler for INT0 - called when the signal on Pin 2 changes.
************************************************************************/

void int0handler() {
// check the value again - since it takes some time to
// activate the interrupt routine, we get a clear signal.
wwvbSignalState = digitalRead(wwvbRxPin); // read pulse level
digitalWrite(ledRxPin, wwvbSignalState); // change Rx indicator LED to match
}

/*******************************************************************************************************
* scanSignal()
*
* Evaluates the signal as it is received. Decides whether we received
* a "mark", a "1" or a "0" based on the length of the pulse.
*
* We're decoding awesome US cold war technology here, no FEC, no parity. Attempt to remove noise spikes.
* Count 1 frame start, 6 marks, 60 bits. Anything else is garbage, triggers a frame reject, clears
* the receive buffer and restarts recording bits.
*
* Hope no bits got flipped.
*
* Secret, remote mount antenna and receiver as far from your workbench as possible in a Ft. Collins, CO
* facing window with loop antenna broadside to that direction. Remove the stock tuned loopstick antenna
* and replace with the long tuned version.
*******************************************************************************************************/

void scanSignal(void){
if (wwvbSignalState == 1) { // see if receiver input signal is still high
int thisFlankTime=millis(); // retrieve current time
previousFlankTime=thisFlankTime; // add time to count
}
else { // or a falling flank
int difference=millis() - previousFlankTime; // determine pulse length
signalNoise = 0; // clear signal noise detected
digitalWrite(ledMarkPin, LOW);
digitalWrite(ledFramePin, LOW);
if (difference < WWVB_noise_millis) { // below minimum - pulse noise
// enough of this and it will cause bit flips and erroneous frame markers
signalNoise = 1;
} else if (difference < WWVB_mark_millis) { // 10 second and frame markers
// two sequential marks -> start of frame. If we read 6 marks and 60 bits,
// we should have received a valid frame
if ((prevMark == 1) && (markCount == 6) && (bitCount == 60)) {
appendSignal(0);
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 = 1; // should be a valid frame
frameError = 0; // set frame error indicator to zero
errorCount = 0; // set frame error count to zero
finalizeBuffer(); // hand off to decode time/date
} else if ((prevMark == 1) && ((markCount != 6) || (bitCount != 60))) { // bad decode-frame reject
digitalWrite(ledFramePin, HIGH);
markCount = 0; // bad start of frame set mark count to zero
prevMark = 0; // clear previous to restart frame
bitCount = 1; // set bit count to one
frameError = 1; // and indicate frame error
errorCount ++; // increment frame error count
bufferPosition = 63; // set rx buffer position to beginning
wwvbRxBuffer = 0; // and clear rx buffer
} else { // 10 second marker
markCount ++; // increment mark counter, 6 per minute
appendSignal(0); // marks count as 0
digitalWrite(ledMarkPin, HIGH);
prevMark = 1; // set mark state to one, following mark indicates frame
bitCount ++; // increment bit counter
}
} else if (difference < WWVB_split_millis) {
appendSignal(1); // decode bit as 1
digitalWrite(ledBitPin, HIGH); // bit indicator LED on
prevMark = 0; // set mark counter to zero
bitCount ++; // increment bit counter
} else {
appendSignal(0); // decode bit as 0
digitalWrite(ledBitPin, LOW); // bit indicator LED off
prevMark = 0; // set mark counter to zero
bitCount ++; // increment bit counter
}
}
}

/************************************************************************************************
* appendSignal()
*
* Append a decoded signal bit to the wwvbRxBuffer and decrement bufferPosition counter.
* Argument can be 1 or 0. The bufferPosition counter shifts the writing position within
* the buffer over 60 positions. Reverse bit order by starting at 63 and working back towards
* 0 to account for MSB0/LSB0 mismach between processor and transmitted data.
************************************************************************************************/

void appendSignal(byte signal) {
// bitwise OR wwvbRxBuffer contents with signal bit shifted to bufferPosition
wwvbRxBuffer = wwvbRxBuffer | ((unsigned long long) signal << bufferPosition);
bufferPosition--; // decrement bufferPosition for next bit
}

/************************************************************************************************
* finalizeBuffer()
*
* Evaluate the information stored in the buffer. This is where the WWVB signal data is evaluated
* and time/date values are decoded from BCD and the internal clock is updated.
************************************************************************************************/

void finalizeBuffer(void) {

byte dmm; // store decoded minutes
byte dhh; // store decoded hours
byte ddst; // store decoded dst flags

// point wwvbBuffer structure at wwvbRxBuffer to allow simpler extraction of data bits to decode
// WWVB time and date information.
struct wwvbBuffer *rx_buffer;
rx_buffer = (struct wwvbBuffer *)(unsigned long long)&wwvbRxBuffer;

// convert the received bits from BCD
dmm = (10 * bcdToDec(rx_buffer->MinTen)) + bcdToDec(rx_buffer->MinOne);
dhh = (10 * bcdToDec(rx_buffer->HourTen)) + bcdToDec(rx_buffer->HourOne);
ddst = bcdToDec(rx_buffer->Dst);
doy = (100 * bcdToDec(rx_buffer->DayHun)) + (10 * bcdToDec(rx_buffer->DayTen))
+ bcdToDec(rx_buffer->DayOne);
year = 2000 + (10 * bcdToDec(rx_buffer->YearTen)) + bcdToDec(rx_buffer->YearOne);
lyr = rx_buffer->Leapyear;

/* DST flags Q'nD, odds are DST, evens standard
* actual flags
* bit 55 dst 00 standard time 10 dst begins tonight Decimal 0 and 2 respectively
* bit 56 dst 11 dst in effect 01 dst ends tonight Decimal 1 and 3 respectively
*/
if (ddst == 3 || ddst == 1) { dst = 1; } else { dst = 0; }

/* 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 (dmm == 59) { // if decoded minute is 59
mm = 0; // set minutes to zero
hh = (dhh + 1 + 24) % 24; // and advance to next hour
} else { // otherwise
mm = dmm + 1; // just add one minute to decoded minutes
hh = dhh;
}

// Reset seconds to zero, buffer position counter to 63 and clear receive buffer
ss = 0;
bufferPosition = 63;
wwvbRxBuffer = 0;
}

// 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("?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("?D31C141C0000000000"); // special character degree symbol
delay(300);
Serial.print("?f"); // clear screen
}

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

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

// read sensor value from LM35 temperature sensor and calculate temperature
// replace sensor with LM34 for degrees Fahrenheit
void getTemperature() {
int sensorValue;
sensorValue = analogRead(tempSensorPin);
calculatedTemperature = (5.0 * sensorValue * 100.0) / 1024.0;
}

// Dip Switch for UTC - read once on startup - polling messes up reading WWVB receive data
void utcSwitchCheck() {
boolean utcVal = digitalRead(utcSwitchPin);
if (utcVal == 1) {
displayUTC = 1 ; // Show UTC
}
}

void tzSwitchCheck() {
boolean tz1Val = digitalRead(tz1SwitchPin);
boolean tz2Val = digitalRead(tz2SwitchPin);
if (tz1Val == 0 && tz2Val == 0) { selectTZ = 0; } // TZ E
if (tz1Val == 0 && tz2Val == 1) { selectTZ = 1; } // TZ C
if (tz1Val == 1 && tz2Val == 0) { selectTZ = 2; } // TZ M
if (tz1Val == 1 && tz2Val == 1) { selectTZ = 3; } // TZ P
}


/********************************************************************************************
* serialDumpTime()
*
* Dump the time, date and current temperature to the serial LCD
********************************************************************************************/

void serialDumpTime(void) {
int tempout;
int tempoutd;
char timeString[12];
char dateString[12];
char tempString[8];

tempout = calculatedTemperature;
tempoutd = ((calculatedTemperature - tempout) * 10);

if (year == 0) {
// Display "Aquiring Frame:" and bit count. At 60, frame is finished and clock should sync.
// Count restarting or going over 60 indicates bad signal reception. Move receiver/antenna
// to better location or try during more radio signal quiescent time of day.
Serial.print("?x00?y0Acquiring Frame:");
Serial.print("?x00?y1");
Serial.print("?0?1?2");
Serial.print("?x07?y1");
if (bitCount < 10) { Serial.print("0"); }
Serial.print(bitCount, DEC);
Serial.print("?x13?y1");
Serial.print("?2?1?0");
Serial.print("?c0");

} else {
// Hour, minutes and seconds
// Flashing seconds colon
if (clockStarted == 0) { clearLCD(); } // first time, clear display
Serial.print("?x00?y0"); // cursor to row one, char 1

// calculate local time from offset and DST flag
lhh = ((hh + dst + 24) - TZ[selectTZ][1]) % 24; // calculate local time
// calculate local hour equivalent to 00hrs UTC so we can correct our day change to 00hrs local
dayxing = ((0 + dst + 24) - TZ[selectTZ][1]) % 24; // calculate local time

/* 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
day = doy - eomYear[eom][lyr];
mon = (eom + 1) % 12;
eom++;
}

// because of the local time offset the date changes early (Ohrs UTC). We need to calculate the
// previous month and day to correct the local time date display.
int peom = 0; // eom counter used to determine previous month and day
while (eomYear[peom][lyr] < (doy - 1)) { // calculate previous month and day for local time offset
prevday = (doy - 1) - eomYear[peom][lyr];
prevmon = (peom + 1) % 12;
peom++;
}


if (displayUTC == 1) { // display either UTC or local time
if ((ss % 2) == 0) {
sprintf(timeString, "%02d:%02d.%02d UTC ", hh, mm, ss);
} else {
sprintf(timeString, "%02d %02d.%02d UTC ", hh, mm, ss);
}
Serial.print(timeString);
} else {
if ((ss % 2) == 0) {
sprintf(timeString, "%02d:%02d.%02d %c", lhh, mm, ss, TZ[selectTZ][0]);
} else {
sprintf(timeString, "%02d %02d.%02d %c", lhh, mm, ss, TZ[selectTZ][0]);
}
Serial.print(timeString);
if (dst == 1) { Serial.print("D"); } else { Serial.print("S"); }
Serial.print("T ");
}

/******************************************************************************************
* frame error indicator - serves as signal indicator - arduino serves as clock
* during the noisy parts of the day, as it starts picking up WWVB frames, it syncs
* to NIST time.
* 1. Continuous full block indicates time as accurate as propagation delay will allow.
* 2. Block and half block alternating. Signal is degrading, sync update when block shows.
* 3. Half block, 5 or less sequential frame errors, low bar more than 5 frame errors.
* 4. Low bar continuous, poor WWVB signal, no sync. Running on microprocessor clock only.
******************************************************************************************/
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"); // ANSI Standard YYYYMMDD Sortable date string
if (displayUTC == 1) { // UTC can directly use month & day
sprintf(dateString, "%04d%02d%02d ", year, mon, day);
Serial.print(dateString);
// Display temperature in degrees Celsus
sprintf(tempString, "%3d.%1d?3C", tempout, tempoutd);
Serial.print(tempString);
} else if (lhh < dayxing) { // Local time can use date info up to UTC date crossing
sprintf(dateString, "%04d%02d%02d ", year, mon, day);
Serial.print(dateString);
// Display temperature in degrees Celsus
sprintf(tempString, "%3d.%1d?3C", tempout, tempoutd);
Serial.print(tempString);
} else { // Delay change of date till 00hrs local
sprintf(dateString, "%04d%02d%02d ", year, prevmon, prevday);
Serial.print(dateString);
// Display temperature in degrees Celsus
sprintf(tempString, "%3d.%1d?3C", tempout, tempoutd);
Serial.print(tempString);
}
clockStarted = 1;
}
}

Summertime RF Propagation and the CMMR-6P-60

Quite a few people have problems with getting the CMMR-6P-60 WWVB Receiver to work. Usually the breakdown in communications is atmospheric or manmade noise.

The 60 kHz signal we're attempting to pull out of the air is an amplitude shift keyed signal (really slow pwm). Our receiver depends on being able to hear a 17db drop in the power to receive a 1 bps binary encoded decimal data stream.

The slow data transmission rate of our received signal allows us to visually monitor the signal quality quite easily. An LED can be driven by the TCON pin on the CMMR-6P board. I've also included an TCO RX indicator LED driven by Arduino Pin 4.

Using either of these LEDs, you should see a once per second flash with the ON/OFF duration depending on whether a zero, one or mark symbol is being received.

If your TCO indicator LED is sitting there flickering like LED 13 does while you're uploading your Arduino sketches, you have a local man-made RFI or weak signal problem. Something is raising the noise threshold up to the point that the receiver is unable to hear WWVB properly or else you are in a location where the signal is being attenuated (metal building, thick concrete walls, basement). The antenna needs to be at least 3-6 feet (even more is better) from noise sources like computer monitors, displays, unshielded microprocessor circuits, fluorescent lights, etc. 60 kHz theoretically should penetrate most walls, but placing it near a window can't hurt. NIST Special Publication 960-14, Page 42-43 has a listing of things to consider that will cause interference or reduced signal strength.

Another thing to consider with placement is that the loopstick antenna used on this receiver is directional. Signal nulls occur off the ends of the rod, maximum signal is received with the antenna positioned broadside to Fort Collins, Colorado. Try to get the noise source in the null in addition to distance from your noise source.



The other receive problem manifests itself with the TCO LED intermittently giving two or more flickers per second instead of the one per second you are expecting (pulse, pulse, pulse, flicker, pulse, pulse, etc). This can be man-made impulse noise such as having a neighbor drive past with a bad ignition harness, motor starts, etc. More likely is atmospheric impulse noise (distant lightning, daytime solar radiation). During the night, the 60 kHz longwave signal propagates further so the signal tends to be strongest then. In summertime, not only do the number of daylight hours increase causing reduced signal propagation (Page 12, table 2), but atmospheric noise increases as well.

We have one trick up our sleeve to deal with impulse noise. We're using a loop antenna which acts as a magnetic sensor probe to pick up our WWVB 60 kHz signal. Our impulse noise tends to propagate as a mostly electric component. This means that if we construct a partial electrostatic shield, we can block a portion of this noise which can allow our receiver to get a few more decodes than would be possible during the noisy part of the day.

Here's our prototype CMMR-6P-60 WWVB receiver with 100mm antenna.


It needs to be spaced at least 3/4 inch away from the shield.


The electrostatic shield is a sheet of aluminum foil curved into a u-shaped trough.


The shield in place. Notice the ground wire that bonds it to our receiver ground.

2009-06-15

Clock Indicators and Switches

Here is a schematic for the indicator LEDs and switches on my current clock project (rewrite). All the LED indicator pin-outs should work with all Arduino -- CMMR-6P sketches posted up-to-date on 'Duino Lab. The switches are a new addition to set UTC/Local Time and Time Zone for Continental US. More on the code revisions later that enable that function. The new Arduino IDE is out at Version 16 and allows posting sketch code as HTML, I'd like to get that running as using <pre> tags to post code on this blog is "USER UNFRIENDLY".

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.