Homemade TCXO

The DS3231 RTC chips are ‘da bomb’! None-the-less, I wanted to make my own Temperature Controlled (X) Oscillator. I used a Tiny 85, a 10K thermistor (similar to this one but smaller) in close contact with an 8MHz crystal with 20pF capacitors. Here is my circuit:

And a picture of the circuit (which I put into an Altoids tin, natch :wink: ):

The crystal case is wrapped in a double layer of cellophane tape with a hole cut out for the thermistor tip, then covered with an insulating layer of hot-melt-glue. The un-glued assembly is shown here:

To calibrate the device, I used four temperature standards: the freezer and beverage drawer of the fridge, room temperature at my girlfriend’s house, and inside the greenhouse at work, for 25, 33, 72 and 100 Fahrenheit degrees, respectively. The millis per hour was measured at each of these temperature, as was the analogRead() value obtained from the thermistor’s voltage divider circuit. This gave me a linear relationship between temperature and the time difference, measured in milliseconds per hour. Here’s the data from my notebook:

My data gives rise to this equation:

  millis()/hour = -0.13*analogRead(thermistor) + 80

Convert millis()/hour to micros()/sec (ppm) and add the factor to the variable ‘oneSecond’, which is nominally one million micros per second, as in the following sketch:

/***********************************************
  Count down using temperature compensated oscillator

  Tiny 85, Adafruit 7-segment, I2C, LED backpack display\
  d2 (pin 7) -> I2C clk (C on 7-segment)
  d0 (pin 5) -> I2C sda (D on 7-segment)
  8MHz crystal on XTAL1 (pin 2) and XTAL2 (pin 3)
  10K inverted output thermistor on RST (pin 1)
  mode switch on d1 (pin 6)

  021217 clh 3042/263 uno
  021617 clh 3044/265
  032117 clh 1808/74  tiny85
  032717 clh 1834/74

  ---> tiny85 #F in green "Hour Glass" tin

************************************************/

#include <TinyWireM.h>
#include "Adafruit_GFX.h"
#include "Adafruit_LEDBackpack.h"
Adafruit_7segment matrix = Adafruit_7segment();

#define modeSwitch 1  // attach a switch to pin 6 (d1)
#define thermistor A0  // attach a thermistor to pin 1 (rst):
//                        The thermistor should be in direct contact
//                        with the metal casing of the crystal, and
//                        isolated from the external environment.

#define startMinute 59  // Modify these defines to
#define startSecond 59  // change the timer interval

// linear equation parameter: microseconds (*0.036)  = m analogRead(thermistor) + b
#define b 80                // m & b values calibrated 2/24/17 - 3/7/17
#define m -13

// switch position
#define countUp false  // closed
#define countDown true  // open

unsigned long previousSecondMicros = 0UL;
long oneSecond = 1000000L;

byte minutes = startMinute;
byte seconds = startSecond;

boolean isColonOn = false;

void setup() {

  pinMode(modeSwitch, INPUT_PULLUP);  // pin 6 --> slide switch --> gnd
  pinMode(thermistor, INPUT);  // pin 1 --> 16K --> Vcc | pin1 --> 10KTherm --> 3K --> gnd

  matrix.begin(0x72);
  matrix.writeDisplay();

  previousSecondMicros = micros();
}

void loop() {

  // --------- Run this code every second ------------------
  if (micros() - previousSecondMicros >= oneSecond) {

    // count up depending on the position of the mode switch
    if (digitalRead(modeSwitch) == countUp) {
      if (seconds++ >= startSecond) {
        if (minutes >= startMinute) {
          minutes = 0;
          seconds = 0;
        } else {
          minutes++;
          seconds = 0;
        }
      }
    }

    // show the count
    matrix.writeDigitNum(0, (minutes / 10));
    matrix.writeDigitNum(1, (minutes % 10));
    matrix.writeDigitNum(3, (seconds / 10));
    matrix.writeDigitNum(4, (seconds % 10));
    matrix.drawColon(isColonOn);
    isColonOn = !isColonOn;
    matrix.writeDisplay();

    // count down if the mode switch is in 'count down position'
    if (digitalRead(modeSwitch) == countDown) {
      if (seconds-- <= 0) {
        if (minutes <= 0) {
          minutes = startMinute;
          seconds = startSecond;
        } else {
          minutes--;
          seconds = startSecond;
        }
      }
    }
    
    // temperature compensation in microseconds (book 6 032117)
    long dt = 0 - ((b +  ((analogRead(thermistor) * m) / 100) * 10) / 36);
    oneSecond = 1000000L + dt;

    // ---- end "Run this code every second" -----
    previousSecondMicros += oneSecond;
  }
}

I ran dozens of tests last week: my hourglass is less than 20ppm inaccurate. I’m feeling good about that, but I also think it could be improved.

This is the Guidance forum, so my supplications are:

1) Is this project stupid? The DS3231 rocks, right? So is re-inventing this wheel an ill-considered idea? It’s no less expensive, but gives me the satisfaction of understanding more of the ‘inner workings’.

Don’t worry - if this totally misses the mark, just tell me.
I’ve invested less than twenty bucks, but have been thinking about this for months. If I should give it up, now would be the time.

2) Is a thermistor the best choice of temperature sensor? I also have TMP35 type sensors, which might fit even tighter on the crystal housing due to it’s flat surface. The bulk is what warned me away, because I associate plastic with insulation.

Are there better sensors?

3) What is the availability and how would I use variable capacitors? Until I read up on how the 3231 works, I’d never heard of them.
Mechanical components with interleaved alternating semi-circular blades, as seen in ancient AM radios not-with-standing.

4…) Are there other design considerations I haven’t addressed? Kind of a wide topic, but does this idea make you think of something I should think about?

Please lemme know.

long dt = 0 - ((b +  ((analogRead(thermistor) * m) / 100) * 10) / 36);

I suspect that truncates. You probably need a “+5” somewhere.

my hourglass is less than 20ppm inaccurate

You may be able to shave that a tad with voltage compensation.

A commercial 10MHz tcvcxo is good for <1ppm for about £15.

Try Magna Frequency.

Allan

[quote author=Coding Badly date=1490684180 link=msg=3196363]

long dt = 0 - ((b +  ((analogRead(thermistor) * m) / 100) * 10) / 36);

I suspect that truncates. You probably need a "+5" somewhere.

You may be able to shave that a tad with voltage compensation.

[/quote]

In the multifactor calculation, dt has a maximum result of 15 and a minimum of -12. The biggest intermediate value is 410, and the smallest is -13312 - all of which fits in an int, then is held in a long so it adds to micros() correctly. I don't think it truncates, but I will recheck the numbers.

I wondered about voltage vs temperature variation. Rats - more calibrating! :o

allanhurst:
A commercial 10MHz tcvcxo is good for <1ppm for about £15.

Try Magna Frequency.

Allan

Thanks for the reference Allan. I will certainly consider them, especially of the pound keeps falling with resect to the dollar! :stuck_out_tongue:

I admit though that this is a conceptual experiment, not really an effort to obtain maximum precision (yet!) I kinda have my eye on eventually making a clock using an atomic oscillator. Laser cooled strontium would be nice, but used rubidium clocks from cell towers are available for a couple hundred bucks. I understand that cesium oscillator based Chip Scale Atomic Clocks are are almost real, but bound to be expensive for a while.

I’ll look into Magna’s voltage compensated crystals, but it seems so incremental. Adding my thermistor gave me several orders of magnitude better precision, so I’m kinda spoiled.