Arduino atomic clock project ideas

Keep in mind - this isn't a radio clock that pulls from a server - this is the real deal, folks.

I'd like to make an atomic clock. Now, I have a rubidium 10MHz standard that I'd like to do something with other than what I'm already using it for - calibration of instruments and determination of drift of oscillators (I'm getting into amateur radio).

They have a maximum stability of 10e-11, so I'd like to build a clock to go along with that. It would make an interesting display as well as a conversation piece.

I'll put up the rest of my thoughts so that way it debunks some other things here. All I need is to have the Arduino be able to very, very accurately read the frequency and time off that, as well as being able to server-uplink to initially set time to NTP-standard. I can use frequency dividers to do that if 10MHz is out-of-range or impractical to use.

What I've considered so far:

  • Replacing the crystal feeding the oscillator of the chipset directly with a BNC feed-in allowing the electronics to clock directly at 10MHz. This initially was very attractive, although I am nervous about the idea of doing so because I don't know how (or if) the microcontroller is set to expect a 16MHz clock rate.

  • Frequency-dividing to 1MHz and feeding that to a port and having the timekeeping ignore the onboard clock rate and time off 1MHz.

I don't know how I'd write a sketch to be able to timekeep on either option, however, and I'd really appreciate guidance on how to run the coding as well as which option (or another one, if there is a better alternative) to take. I'm planning on fully documenting the build if I'm able to complete it and releasing it to the GP.

I can figure out all the interfaces, inputs & outputs if I know the brunt of the sketch I'll be using.

TIA,
Eli Fedele
(djkolumbian)

You're worried that the microcontroller won't work well at 10Mhz even though it works fine at 16Mhz? You are worrying about the wrong thing. The main problem is that the output from that device is only about +-0.5V and it's a sine wave. It's not a +5V/0V or +3.3V/0V square wave. But, it looks like there is a very easy fix. Read this, it gives some very specific advice: (note that you have to have your fuses set correctly to use this kind of clock input)

http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&p=643736

The magic:

Set the CKSEL to 0111 (Full swing xosc) and feed your LPRO output to XTAL1 pin via the 1..10 nF capacitor. Check the XTAL2 output with an scope - you should get a clean and almost square waveform out there which indicates the Mega is clocked properly.

This trick makes an amplifier out of the XTAL1->XTAL2 invertor which is self biased to a linear gain region due to an internal feedback resistor which is connected only in internal crystal CKSEL modes.

Why do you need the resolution to a microsecond or a tenth of one anyway, are you measuring an exploding atomic bomb? I think a 1 pulse per second clock would be fine. Hey, look at this:

http://www.leapsecond.com/pic/picdiv.htm

You may need some sort of amp to get the voltage high enough for this chip.

As for how you would actually consume the clock, you would use either interrupts or counters. Interrupts are not going to work at 10Mhz or 1Mhz, the counter should work fine. Here is some information:

https://sites.google.com/site/qeewiki/books/avr-guide/counter-atmega328

Maybe the best bet would be to make a little amp to raise the voltage of this signal, pass it through to T1, and see if you can get some code going to keep track of the count. And if that doesn't work well, reduce it to a 1PPS signal and just put that on an interrupt line and keep track of it via interrupts.

Anyway, here is my rubidium. You can clearly see these are low-voltage signals.

JoeN:
You're worried that the microcontroller won't work well at 10Mhz even though it works fine at 16Mhz? You are worrying about the wrong thing. The main problem is that the output from that device is only about +-0.5V and it's a sine wave. It's not a +5V/0V or +3.3V/0V square wave. But, it looks like there is a very easy fix. Read this, it gives some very specific advice: (note that you have to have your fuses set correctly to use this kind of clock input)

http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&p=643736

The magic:

Set the CKSEL to 0111 (Full swing xosc) and feed your LPRO output to XTAL1 pin via the 1..10 nF capacitor. Check the XTAL2 output with an scope - you should get a clean and almost square waveform out there which indicates the Mega is clocked properly.

This trick makes an amplifier out of the XTAL1->XTAL2 invertor which is self biased to a linear gain region due to an internal feedback resistor which is connected only in internal crystal CKSEL modes.

Now on to the more important point - just because you are running your AVR at 10Mhz doesn't mean you are going to be able to do anything useful with that. It's not like you can do anything substantial in a single clock cycle anyway. It's almost like you are pretending your AVR is a counter and still has cycles left over to do the work of displaying the clock time and accepting inputs and really it won't. Probably if you want to use an AVR exclusively, you will want to divide this down to something even less than 1Mhz. Why do you need the resolution to a microsecond anyway, are you measuring an exploding atomic bomb? I think a 1 pulse per second clock would be fine. Hey, look at this:

picDIV -- Single Chip Frequency Divider

You may need some sort of amp to get the voltage high enough for this chip.

As for how you would actually consume the clock, you would use either interrupts or counters. Here is some information:

Sign in - Google Accounts

Anyway, here is my rubidium. You can clearly see these are low-voltage signals.

Wow, I've spent a day of searching and haven't come across that yet...makes me look like a fool.

The only reason I had doubt of the external oscillator input was I'm still relatively new to digital systems (I've been building analog designs for a long while now), and I wasn't sure how the command structure would handle under the reduced clock rate.

How would I modify the device for external osc use? Is this sketch-level coding or deeper?

Now, is the modification process as simple as simply desoldering and jumping an input to the onboard crystal leads? I'd tend to think so knowing that crystals ideally output sine waves in the first place. Dividing to as low as 1Hz is fine. I just need second-level resolution. I'm not building a DIY GPS satellite on the side.

How to generate appropriate osc gain isn't too difficult. I could always feed it to a RF op-amp and swing the voltage +/- 2.5V, feed a bias line hot at +2.5V, there's your 5-0V swing. Feed through filter to get square wave. But, simpler options first. I'll take the above.

Power supplies are also no issue. I've dealt with RF amps running no less than 3,500 before.

That being said, feeding it a 1Hz derivative waveform, would I simply program it to count the seconds on spikes and move from there?

I guess that the easiest way to approach this is to attempt to drive the 328P using the 10Mhz wave as described in the hack. Then what you want to do is use one of the hardware timers to keep track of your time. I think an easy way to do this might to be to use the 16-bit timer (Counter1) with it set up to call an interrupt service routine and reset every time it hits 10,000. That way you would get an interrupt every 1ms and you would increment your time in your ISR. Do everything else in your main loop. This has some very good explanations and source code:

When I was thinking aloud earlier I think I reached for a few solutions that were more complex than necessary.

How does this look for a start?
I'll be using the Arduino Mega.

volatile int sec;

void setup()
{
  attachInterrupt(0, Increment, LOW);
}

void loop()
{
  //Processing code goes here.
}

void Increment()
{
  sec = sec + 1;
}

How would I continue to process the code from there? I'm thinking:

if(sec = 60)
{
   sec = 0;   
   min = min + 1;
}

But that doesn't seem right, although it sounds conceptually like it'd reset the variable. It seems like that would be telling the board one thing is also another. Please advise.

If you can divide the sine wave down to 1 Hz, can't you just offset it so it's a positive voltage and run it to an analog pin and poll the voltage on the pin? Set some threshold and increment the second count whenever it passes the threshold.

according to ebay...

this rubidium frequency gen is programmable down to 1hz

TanHadron:
If you can divide the sine wave down to 1 Hz, can't you just offset it so it's a positive voltage and run it to an analog pin and poll the voltage on the pin? Set some threshold and increment the second count whenever it passes the threshold.

Interesting...I hadn't thought of it like that. I might do that. It wouldn't be hard to run a bias tap and push it "hot" so the full voltage is into the positive voltage range. Cue a voltmeter, determine max output, divide by 5, multiply by 1024. Set threshold to say, 98% of that to account for nominal voltage fluctuations.

Dropping to 1Hz would also allow me to specifically quantify the drift as it changes and introduce code to factor the automatic adjustments in.

cjdelphi:
according to ebay...

this rubidium frequency gen is programmable down to 1hz

Yes, but then I lose the ability to also have the 10MHz standard for instrument/VFO/VXO calibration running simultaneously.

Or maybe you could fire up the analog comparator input capture unit, and try for a "zero crossing" timestamp. I've never done that but I understand it can get you accuracy close to one clock cycle.

TanHadron:
Or maybe you could fire up the analog comparator input capture unit, and try for a "zero crossing" timestamp. I've never done that but I understand it can get you accuracy close to one clock cycle.

What I realized is that if I divide to 1Hz, any drift will throw the time off very noticeably on the order of hours from start.

Instead, suppose I divide by 10 to 1MHz to give myself headroom with the clock (either 1:10 or 1:16) - then have it read the state of the input (HIGH and LOW) and look for a million HIGH states?

Not triggering until 10,000,000 cycles have elapsed would not be hard. Declare cycleState as a long value, have the counter dump it every minute or so.

I could have it, for example:

if(cycleState % 10000000 = 0)
{
sec = sec + 1;
}

That would allow local variations in 1Hz to remain at 1 millionth of accuracy differential. If the coding is made to be somewhat concise in the loops needed then the program will have no problem executing them in the time given.

Although personally, this is jeopardizing the accuracy of the source by a factor of 10. Reading the state and pumping numbers based on that will give a error number (stability number) that at best is 1:10th of what it should be.

I would much rather clock off an intrinsic function, with the oscillator configured to be super-stable sourced, so that the coding can handle the best way to do it. Going the above route feels like it would be jimmy-rigging a counter that appears to be time. Eventually the smallest variable would go too high and the counter would stop.

Can anyone shed light on intrinsic functions that use the clock rate as a reference? Does the Time library keep time based on its clock rate?

I see two issues with that.

First, if you're running standard 16 MHz clock, you would only have 16 clock cycles to handle each interrupt. Or low/high transition however you plan to tackle it. That's pretty tight. You might want to divide by 100 or 1000 or even 10000 to give you some room to run some code. The drift shouldn't be a problem if the divider works well. It will make it up in the next cycle.

Second, that method of checking for 1000000 might not be very reliable. If you happen to be doing something that takes longer than 16 clock cycles, and you don't run that code during the 1 microsecond the counter has exactly that value, you'll miss the entire second. You should probably have something in there with a >= operator, and set some flag or timestamp when you handle each second.

The time functions use the overflow interrupt on timer0, which uses the system clock divided by a prescaler of 64.

Theoretically, if you get all your headers right, it should calculate all the numbers correctly and give you accurate millis() and micros() results. The only thing that worries me is this comment in wiring.c:

// the fractional number of milliseconds per timer0 overflow. we shift right
// by three to fit these numbers into a byte. (for the clock speeds we care
// about - 8 and 16 MHz - this doesn't lose precision.)
#define FRACT_INC ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3)
#define FRACT_MAX (1000 >> 3)

I think the simplest options are:

  1. Use the device to clock the mcu at 10MHz using the trick already described i.e. making the crystal oscillator circuit work as an amplifier. Unfortunately, the Arduino millis and micros functions don't give the right results with F_CPU = 10000000 afaik.

  2. Convert the 10MHz sine wave to a square wave using a comparator, divide it by (e.g.) 100, and feed it into one of the interrupt pins to get an interrupt every 10us.

  3. Convert the 10MHz sine wave to a square wave using a comparator, divide it by 4 using a 74HC74, feed it into the TOSC1 pin and use it to clock Timer 2. Set up Timer 2 to divide by (e.g.) 250 to get an interrupt every 100us.

OK, I got this going based on the direct clocking idea and information I found right here on this board concerning the 16-bit timer. The scheme is to have an interrupt go off every 10,000 clock cycles and count that in an ISR. So I am counting milliseconds here. There is no GPS synchronization or date arithmetic to convert the seconds into a useful time and date or anything like that, just the most basic proof of concept. You can infer the schematic from the code.

// Define GPIO pins.

const int pauseButton = 0; 
const int digit1 = 1; 
const int digit2 = 2; 
const int digit3 = 3; 
const int digit4 = 4; 
const int digit5 = 5; 
const int digit6 = 6; 
const int digit7 = 7; 
const int digit8 = 8; 
const int segA = 9;
const int segB = 10;
const int segC = 11;
const int segD = 12;
const int segE = 13;
const int segF = 14;
const int segG = 15;
const int segDP = 16;

volatile long count = 0;
volatile int nocount = 0;
int dispcount = 0;

void setup() {
  // Configure GPIO pins.
  
  pinMode(pauseButton, INPUT); 
  pinMode(digit1, OUTPUT); 
  pinMode(digit2, OUTPUT); 
  pinMode(digit3, OUTPUT); 
  pinMode(digit4, OUTPUT); 
  pinMode(digit5, OUTPUT); 
  pinMode(digit6, OUTPUT); 
  pinMode(digit7, OUTPUT); 
  pinMode(digit8, OUTPUT); 
  pinMode(segA, OUTPUT);
  pinMode(segB, OUTPUT);
  pinMode(segC, OUTPUT);
  pinMode(segD, OUTPUT);
  pinMode(segE, OUTPUT);
  pinMode(segF, OUTPUT);
  pinMode(segG, OUTPUT);
  pinMode(segDP, OUTPUT);
  
  // We aren't going to use segment DP.
  
  digitalWrite(segDP, LOW);
  
  // Set up our timer so our ISR gets called every microsecond.
  
  noInterrupts();
   
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1AH = 39; // Trigger interrupt at 9,999 (10,000 cycles).  39*256 + 15 = 9,999
  OCR1AL = 15; 
  TCCR1B |= (1 << WGM12); // CTC mode
  TCCR1B |= (1 << CS10); // prescaler of 1
  TIMSK1 |= (1 << OCIE1A); // use interrupts
    
  interrupts();
}

// Our interrupt is called, bump our counter.

ISR(TIMER1_COMPA_vect) 
{
  if (nocount==0)
    count ++;
}

void loop() {
  // Display counter is separate from main counter so pausing count via the pause button doesn't pause display refresh.  But disconnecting the clock will, as it must.
  
  dispcount ++;
    
  if (dispcount==8) dispcount = 0;
 
  // Depressing the pause button will cause the ISR routine to stop counting even though it will continue being called.

  nocount = !digitalRead(pauseButton);

  // Display Count - illuminate one of the digits each time through to effect multiplexing.

  // Turn off our current display so as we set it up we don't get ghosting.
  
  switch (dispcount) {
    case 0:
      digitalWrite(digit8, LOW);
      break;
    case 1:
      digitalWrite(digit1, LOW);
      break;
    case 2:
      digitalWrite(digit2, LOW);
      break;
    case 3:
      digitalWrite(digit3, LOW);
      break;
    case 4:
      digitalWrite(digit4, LOW);
      break;
    case 5:
      digitalWrite(digit5, LOW);
      break;
    case 6:
      digitalWrite(digit6, LOW);
      break;
    case 7:
      digitalWrite(digit7, LOW);
      break;
  }
  
  // Illuminate segments for the current digit.
  
  // Figure out digits for current display and current count, i.e. ones digit for display zero, tens digit for display two, ten millions digit for display eight.

  long digit = count;
  
  for (int i=0;i<dispcount;i++)
  {
    digit /= 10;
  }
  
  digit = digit % 10;

  // Actual segment map courtesy of the Arabs.
  
  switch (digit) {
    case 0:
      digitalWrite(segA, HIGH);
      digitalWrite(segB, HIGH);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, HIGH);
      digitalWrite(segE, HIGH);
      digitalWrite(segF, HIGH);
      digitalWrite(segG, LOW);
      break;
    case 1:
      digitalWrite(segA, LOW);
      digitalWrite(segB, HIGH);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, LOW);
      digitalWrite(segE, LOW);
      digitalWrite(segF, LOW);
      digitalWrite(segG, LOW);
      break;
    case 2:
      digitalWrite(segA, HIGH);
      digitalWrite(segB, HIGH);
      digitalWrite(segC, LOW);
      digitalWrite(segD, HIGH);
      digitalWrite(segE, HIGH);
      digitalWrite(segF, LOW);
      digitalWrite(segG, HIGH);
      break;
    case 3:
      digitalWrite(segA, HIGH);
      digitalWrite(segB, HIGH);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, HIGH);
      digitalWrite(segE, LOW);
      digitalWrite(segF, LOW);
      digitalWrite(segG, HIGH);
      break;
    case 4:
      digitalWrite(segA, LOW);
      digitalWrite(segB, HIGH);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, LOW);
      digitalWrite(segE, LOW);
      digitalWrite(segF, HIGH);
      digitalWrite(segG, HIGH);
      break;
    case 5:
      digitalWrite(segA, HIGH);
      digitalWrite(segB, LOW);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, HIGH);
      digitalWrite(segE, LOW);
      digitalWrite(segF, HIGH);
      digitalWrite(segG, HIGH);
      break;
    case 6:
      digitalWrite(segA, HIGH);
      digitalWrite(segB, LOW);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, HIGH);
      digitalWrite(segE, HIGH);
      digitalWrite(segF, HIGH);
      digitalWrite(segG, HIGH);
      break;
    case 7:
      digitalWrite(segA, HIGH);
      digitalWrite(segB, HIGH);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, LOW);
      digitalWrite(segE, LOW);
      digitalWrite(segF, LOW);
      digitalWrite(segG, LOW);
      break;
    case 8:
      digitalWrite(segA, HIGH);
      digitalWrite(segB, HIGH);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, HIGH);
      digitalWrite(segE, HIGH);
      digitalWrite(segF, HIGH);
      digitalWrite(segG, HIGH);
      break;
    case 9:
      digitalWrite(segA, HIGH);
      digitalWrite(segB, HIGH);
      digitalWrite(segC, HIGH);
      digitalWrite(segD, LOW);
      digitalWrite(segE, LOW);
      digitalWrite(segF, HIGH);
      digitalWrite(segG, HIGH);
      break;
  }

  // Now switch on the common cathode of the current display.
  
  switch (dispcount) {
    case 0:
      digitalWrite(digit1, HIGH);
      break;
    case 1:
      digitalWrite(digit2, HIGH);
      break;
    case 2:
      digitalWrite(digit3, HIGH);
      break;
    case 3:
      digitalWrite(digit4, HIGH);
      break;
    case 4:
      digitalWrite(digit5, HIGH);
      break;
    case 5:
      digitalWrite(digit6, HIGH);
      break;
    case 6:
      digitalWrite(digit7, HIGH);
      break;
    case 7:
      digitalWrite(digit8, HIGH);
      break;
  }
  
  // A little time to allow the LEDs to stay illuminated.
  
  delay(1);
}

The code could stand to be reviewed by someone familiar with ATMega328P counters. This is my first project where I have used one.

Sweet!

  delay(1);

You're counting microseconds, yet delaying a millisecond between displays? Hmmm.

TanHadron:
Sweet!

  delay(1);

You're counting microseconds, yet delaying a millisecond between displays? Hmmm.

Good point, I had that in there initially because the multiplexing looked poor when the display wasn't given some time to "stay on". It was never my intention to be accurate to the millisecond on the display because I don't think that is useful information anyway. Indeed, that millisecond (rightmost) display could be off even without the delay because of all the GPIO and integer math the AVR is doing via the very inefficient Arduino libraries. Maybe I should not show the milliseconds digit since I am doing that. Now, that does not affect the accuracy of the count on the 16 bit counter itself, of course. ISRs will be called even if the main loop is spinning in a delay loop. Of course, the counter code may be crap, that is something I want someone with AVR counter experience to check.

I made a cheap clock using the PPS pin on Ultimate GPS. Much more accurate than uSecond.

sbright33:
I made a cheap clock using the PPS pin on Ultimate GPS. Much more accurate than uSecond.

Pics or it didn't happen! (i.e. upload a photo of your project!)

It did not have a fancy display only a blinking LED. There was only one additional wire to GPS beyond the normal configuration. The code proves the adjusted accuracy of the Uno clock, but it is difficult to read. To use the accurate PPS signal, you only need one line of code digitalRead().