WWVB – Accurate time reception via a radio receiver

I’ve been working on a project that integrates a few ideas into one really cool project. The details of the project itself will become apparent in a few more posts as I get closer to completing the project, but I don’t want to put it all out there at once. It would make this post overly long, so please be patient as the details are revealed.

Part of this project depends on using a WWVB receiver to receive an accurate time/date signal from the broadcast station in Colorado, USA. The NIST time signal is a national radio signal that propagates along the ground throughout much of the United States. This signal is just a simple carrier wave that’s modulated to provide a digital stream of time data to anyone with a receiver. Once the signal is fully received and decoded, you have a time reference that’s just as accurate as your GPS. The nice thing about using WWVB is that the signal easily propagates through walls and you don’t need a clear view of the sky.

For this project, I’m using a WWVB receiver module I purchased at Sparkfun.com. This module only requires three connections: power, ground, and signal out. No complicated interfaces here, that’s for sure.  You may note in the comments on the Sparkfun site that if you live on the east coast, it’s recommended to get the longer loopstick antenna from Digikey.  I should note here that I live in western NY and I’m having no problems receiving the signal between the hours of 10pm (22:00 hours) and 12:30pm UTC.  My receiver isn’t even near a window.  The signal, in my case, has to penetrate through two walls of my house to be received and that’s not even considering the fact that there are a lot of other houses in my neighborhood that the signal has to get through.

WWVBReceiver-300x238

So, now that we have our radio receiver, we need a microcontroller and some code to be able to receive and decode the signal.  I chose an arduino pro mini for the microcontroller platform.  It’s simple to program, tiny, and more than capable of doing what I need for this project.  Of course, I also had to add a regulated 5V supply and a few other parts to make the project complete.  More on that later.

As for code, there are many examples online to look at.  I searched and found several examples, but I didn’t like the way they were written.  Very few, if any, showed good programming practices such as code reuse and small, tightly maintained functions that were clearly commented and documented.  Being frustrated with this situation, I wrote my own code from scratch.  It was easier than I expected:

note: wordpress does not make posting C code easy. If you notice any clipped or improperly formatted sections in the code below, let me know so I can fix it.:

#include 

#define wwvb 10  // input pin

#define pulseMarker 0 // all because processing doesn't understand 'enum'...
#define pulseOne 1
#define pulseZero 2
#define pulseInvalid -1

int8_t wwvbLastState;
int8_t ledPin = 13;
int8_t ledState = 0;

struct WWVBFRAME
{
  int8_t minutes;
  int8_t hours;
  int16_t doy;
  int16_t year;
  int8_t leapYear;
};

void setup()
{
  Serial.begin(9600);
  pinMode(wwvb, INPUT);
  pinMode(ledPin, OUTPUT);

  // At the start of our program, we have no idea what state our input is in
  //  or even where we are in the sequence.  Wait for two consecutive
  //  markers...
  wwvbLastState = digitalRead(wwvb);
  while (digitalRead(wwvb) == wwvbLastState) {} // wait for a transition to
                                                //  happen

}

/* note:  0.8S is a marker.  There are two to three consecutive markers at
              the start of the minute.
          0.2S is a zero
          0.5S is a one

          The signal transitions low for the start of each pulse.

          In this program, pulse types are represented as an int value
          according to the following:  0 = marker; 1 = logical one;
          2 = logical zero.  An enum would have been a more elegant solution,
          but processing doesn't have enums as far as I can tell (gives a
          compile error).
*/

void loop()
{

  // Rant:
  // Somethings *really* not right when you can declare this variable *two*
  //  different ways *and* one of those ways is kinda broken.  For instance,
  //  saying "WWVBFRAME wwvbFrame" works if you never have to pass it to a
  //  function.  To get passing to a function to work, you have to change the
  //  declaration to "struct WWVBFRAME wwvbFrame".  This is broken, imho.
  //  Either complain ALL THE TIME about the incorrect declaration or ACCEPT
  //  BOTH.  The last thing I want to do is to waste time debugging code that
  //  fails mysteriously without any explanation!
  struct WWVBFRAME wwvbFrame;
  int8_t dataValid = 0;

  resetWWVBFRAME(&wwvbFrame);

  while(1)
  {
    if (dataValid > 0)
    {
      Serial.print("valid: ");
      showTime(&wwvbFrame);
    }
    resetWWVBFRAME(&wwvbFrame);
    dataValid = 0;

    waitMinuteMarker(); // wait for next frame start...

    wwvbFrame.minutes = wwvbGetMinutes();
    if (wwvbFrame.minutes < 0) continue; // error - go back to the beginning
                                         //  of the while loop  

    wwvbFrame.hours = wwvbGetHours();
    if (wwvbFrame.hours < 0) continue;

    wwvbFrame.doy = wwvbGetDayOfYear();
    if (wwvbFrame.doy < 0)  continue;

    if (wwvbSkipDUT1() < 0) continue;

    wwvbFrame.year = wwvbGetYear();
    if (wwvbFrame.year < 0) continue;

    wwvbFrame.leapYear = wwvbGetLeapYear();
    if (wwvbFrame.leapYear < 0) continue;     // If we made it here, we have
      dataValid = 1;                                       // valid data
  }
} 

void resetWWVBFRAME(struct WWVBFRAME *w)
 {   
  w->minutes = -1;
  w->hours = -1;
  w->doy = -1;
  w->year = -1;
  w->leapYear = -1;
}

void showTime(struct WWVBFRAME *w)
{
  Serial.print("DOY: ");
  Serial.print((int) w->doy);
  Serial.print(" Year: ");
  Serial.print((int) w->year);
  Serial.print(" LeapYear: ");
  Serial.print((int) w->leapYear);
  Serial.print(" Time(UTC): ");
  Serial.print((int) w->hours);
  Serial.print(":");
  Serial.println((int) w->minutes + 1);
}

int8_t wwvbGetMinutes(void)
{
  int8_t minutes = 0; int8_t temp = 0;
  minutes = wwvbGetNumber(3, 10);
  if (minutes < 0) return minutes;

  temp = getPulseByType(); // discard unused pulse

  temp = wwvbGetNumber(4, 1);
  if (temp < 0) return temp;  

  minutes += temp;
  return minutes;
}

int8_t wwvbGetHours(void)
{
  int8_t hours = 0; int8_t temp = 0;

  temp = getPulseByType();
  if (temp != pulseMarker) return -1;

  temp = getPulseByType();  // discard unused pulses
  temp = getPulseByType();

  hours = wwvbGetNumber(2, 10);
  if (hours < 0) return -1;

  temp = getPulseByType(); // discard unused pulse

  temp = wwvbGetNumber(4, 1);
  if (temp < 0) return -1;  

  hours += temp;
  return hours;
}

int16_t wwvbGetDayOfYear(void)
{
  int16_t doy = 0; int8_t temp = 0;

  temp = getPulseByType();
  if (temp != pulseMarker) return -1;

  temp = getPulseByType();  // discard unused pulses
  temp = getPulseByType();

  doy = wwvbGetNumber(2, 100);
  if (doy < 0) return -1;

  temp = getPulseByType(); // discard unused pulse

  temp = wwvbGetNumber(4, 10);
  if (temp < 0) return -1;
  doy += temp;

  temp = getPulseByType();
  if (temp != pulseMarker) return -1;

  temp = wwvbGetNumber(4, 1);
  if (temp < 0) return -1;

  return doy + temp;
}

// DUT1 is not used in this project.
int8_t wwvbSkipDUT1(void)
{
  // note: it may be a good idea to beef up the error checking here
  int8_t i;
  for (i = 0; i < 5; i++) // two unused pulses plus the DUT1 sign bits
    getPulseByType();     // skip over them...

  int8_t temp = getPulseByType(); // verify marker
  if (temp != pulseMarker) return -1;

  for (i = 0; i < 4; i++) // skip the DUT1 value
    getPulseByType();

  return 0;
}

int16_t wwvbGetYear(void)
{
  int16_t year; int8_t temp;
  temp = getPulseByType(); // discard unused bit

  year = 2000 + wwvbGetNumber(4, 10);
  if (year < 2000) return -1;

  temp = getPulseByType();
  if (temp != pulseMarker) return -1;

  temp = wwvbGetNumber(4, 1);
  if (temp < 0) return -1;
    return year + temp;
} 

int8_t wwvbGetLeapYear(void)
{
  uint8_t leapYear= getPulseByType(); // unused pulse

  leapYear = getPulseByType();
  if ((leapYear == pulseInvalid) ||
      (leapYear == pulseMarker))
    return -1;
  else if (leapYear == pulseOne)
    return 1;

  return 0;
}

/* wwvbGetNumber() - this is one of the core functions that makes the
    whole thing work.  This function gets called to retrieve a sequence
    of pulses from the wwvb stream and convert those pulses into an
    integer.  Typically, the numbers are encoded as binary bits and their
    position in the stream determines if they are to be multiplied by 1,
    10, or 100.  In addition, there is some error checking going on to make
    sure we are receiving a valid signal from teh receiver.  The count
    variable is expected to start at 1 instead of 0 and indicates the number
    of pulses to be evaluated.  As is common in the rest of the code,
    we return -1 if we detect an error.
*/
int16_t wwvbGetNumber(int8_t count, int multiplier)
{
  int8_t i; int8_t result = 0; count--;
  for (i = count; i >= 0; i--)
  {
    int8_t pulse = getPulseByType();
    if ((pulse == pulseInvalid) ||
        (pulse == pulseMarker))
      return -1;

  // Note: Lesson learned: never send a floating point function to do an
  //    integer operation.  In converting the numbers, I originally used
  //    a conversion trick I had learned from programming on pc's where
  //    you set up a loop counting down to zero and use the counter variable
  //    with the pow() function from the math library to determine the value.
  //    In our case, a one in the data stream indicates a value of 2^i.
  //    However, in 'arduino-land', this does *not* work. Somehow when you
  //    try to compute anything equal to or above 2^2, a rounding error
  //    occurs and internally you get back something like 3.9 which, when
  //    cast back to an int, gives you 3 instead of 4!  Bit shifting is fool
  //    proof.

 if (pulse == pulseOne)
      result = result + (multiplier * (1 < 900)          // error conditions. Pulse must be between 150 
     return pulseInvalid; //   and 900mS long. 
   else if (pw  650)
     return pulseMarker;  
   else if (pw > 350)
     return pulseOne;  
   else 
     return pulseZero;  
}

The above code has been fully debugged and works great.  Sure, it could use some improvements like better error checking in a few places, but it handily gets the job done.  I’ve left in some comments that I used to help me remember how to decode the signal that I could have removed, but then thought it would probably make the program more readable if I left them in.

In our next post, we’ll be integrating this with a real time clock module so that we can have an updated and accurate source of time 24 hours a day.  Currently, atmospheric and man-made noise make it impossible to depend on a solid signal throughout the day.  However, I can say that I do get at least one valid WWVB frame per day, so that coupled with a RTC module should give me time keeping abilities that are more than close enough for my project.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s