Simple GPS Disciplined 10MHz Reference uses Dual PWMs

Article By : Damian Bonicatto

The trick to getting a more precise and accurate 10 MHz signal is to find the right adjustment for the VCXO and to readjust it as the VCXO drifts due to various factors.

I was recently working on an experiment for a new design and was using a function generator. Although new function generators have pretty good frequency tolerance specs, I needed something with more accuracy and precision. The way to get more accuracy in frequency generators, arbitrary waveform generators, and frequency counters is to use the 10 MHz reference input provided on many of these test instruments. Not having such a reference, and having been recently playing with the Arduino Nano, I decided it may be interesting to see if one could be built around the Nano.

Figure 1 shows the main parts of this simple and inexpensive, “no calibration required” design. The frequency is generated by a 10 MHz voltage-controlled crystal oscillator (VCXO). This oscillator generates a frequency around 10 MHz with reasonable tolerance but also has a frequency adjust pin that allows for an adjustment up or down. The trick to getting a more precise and accurate 10 MHz signal is to find the right adjustment for the VCXO and to readjust it as the VCXO drifts due to various factors.  Let’s take a look at the way to accomplish this.

Figure 1 The main parts of a simple frequency generator with no calibration required.

The Nano does not have a DAC but as it turns out, one of the simple capabilities of the Nano is controlling its pulse-width modulation (PWM) lines which are intended to be used as an adjustable DC output voltage (after filtering). A simple set of calls will output a 5V PWM signal at a fixed frequency.  The PWMs have an 8-bit adjustment range, so if you want a 2.5V output, you would set the PWM to 128 (50% duty cycle). (Note, for this set of calls, the Nano uses a frequency of around 500 Hz, although this is only important to the filter design.) So, the first thought is to use a PWM output from the Nano, filter it to get a relatively ripple free DC level, and apply this to the VCXO’s adjust pin. This would work, but the issue is that we would only have 255 discrete levels that it could be set to which may not be enough. Let’s take a deeper look at this.

The VCXO I chose for the project is a Taitien TSEAACSANF-10.000000. It has an initial frequency tolerance of 2 ppm and a maximum adjustment range, referred to as “pull range”, of approximately ±12 ppm using 0.5 to 2.5 volts on the adjust pin. So, if we used the full 255 steps of the PWM, we would be able to adjust the VCXO with a resolution of about (2*12/255 ≈ 0.1 ppm).  This did not seem like much of an improvement in ppm accuracy, so I came up with the idea of using two PWMs: one for coarse adjust and one for fine adjust. To keep the design simple, I opted to use resistors only and no op amps. This portion of the circuit can be seen in Figure 2 (there are also 2 filter caps shown which we will discuss later).

Figure 2 Course and fine adjustment to the PWM lines that are intended to be used as an adjustable DC output voltage.

Essentially what you see is an averaging circuit with 2 kΩ of resistance in the coarse portion of the circuit and 270 kΩ in the fine portion.  The final 2 kΩ resistor is used to divide the voltage down as the adjustment pin, as a reminder, this takes 2.5V max. The VCXO adjustment voltage may be solved easily using superposition and is approximately equal to Equation 1.

VCXO adjustment voltage = 9.76×10-3 x PWMCOARSE + 144×10-6 x PWMFINE (Eq. 1)

Where PWMCOARSE and PWMFINE can range in value from 0 to 255. This circuit is sometimes referred to as a passive averaging circuit, as described by Millman’s theorem. Now, making the output voltage to the VCXO adjust pin monotonic using both PWMs is impractical as the precision of the resistors would need to be very high. This is solved by picking resistor values that allow for a fine PWM adjustment to adjust with a range overlapping the following coarse range.

Let me give an example: the resistor values shown in Figure 2 allow for the coarse PWM signal to move about 9.8 mV with each step. The fine PWM signal moves about 0.14 mV per step or roughly 36 mV over its full range. Therefore, since the fine adjustment range is larger than the coarse adjustment step size, there is always plenty of overlap.

Figure 3 shows a couple of the coarse ranges and how the fine value overlaps are configured. In the later discussion of the adjustment algorithm, I’ll explain how this overlap is used.  (Note, the two 100uF caps in Figure 2 had the values selected to keep the ripple from the PWM waveform below the 0.14 mV fine step size. To aid in the filtering, the code increases the PWM frequency to roughly 31 kHz.)

Figure 3 Diagram illustrating fine adjustment range and coarse adjustment step size overlaps.

Putting all this together, the operation goes something like this: after power-up, the Arduino Nano sets the coarse and fine PWMs to an initial value (somewhere in the middle), and then waits until it senses the 1PPS signal coming in from the GPS module. Then, at the interrupt of the next 1PPS signal, the Nano resets the 32-bit counter. The counter then begins counting the cycles from the VCXO. Upon receiving the following 1PPS interrupt, the Nano captures and reads the 32-bit counter, which should be a count of 10,000,000. If the count is less than 10,000,000, it adjusts the fine PWM value up. If the fine PWM value is at 255 before adjustment, the coarse PWM value is incremented and the fine PWM value is set to 127 (in the middle). If the count is greater than 10,000,000, it adjusts the fine PWM value down.  If the fine PWM value is at 0 before adjustment, the coarse PWM value is decremented and the fine PWM value is set to 127 (in the middle). You can see that the overlap of the fine adjustments with more than one coarse value makes this algorithm safe from hunting for an appropriate value, albeit somewhat inefficient in terms of search speed.

This is actually the first stage of adjustment. If we only measured one second of counts, we couldn’t get more than 0.1 ppm accuracy.  So, the second stage of code allows the 32-bit counter to accumulate for 10 seconds by waiting until it has seen 10 interrupts. Now the count can go up to 100,000,000 and we can adjust to get up to the 0.01 ppm range (or 10 ppb). These first two stages are used to close in on the target 10 MHz a bit faster. A third, and continuous operating stage, counts for 100 seconds, waiting for 100 interrupts, before reading the counter. This gets us to an expected count of 1,000,000,000 which allows for an adjustment to 1 ppb. But what can the PWMs actually adjust to? As stated before, the fine PWM moves by roughly 0.14 mV per step and the VCXO adjust by about +/-12 ppm over 0.5 to 2.5 V.  Therefore, each fine step moves the frequency by around (212 ppm(0.14mv/2.0V) ) ≈ 0.0017 ppm or, 1.7 ppb. So, it appears we have enough adjustment to get about ±1 ppb.  There is more to this as can be seen in the code, but this is the essence of the hunt for a precise and accurate 10 MHz. (It should be noted at this point, the Arduino Nano actually has a 16 bit PWM but the maximum frequency is so low that the required filter would have a very long settling time; too long for this application.)

As for performance, the Nano outputs various statistics on the LCD and I typically have a mean frequency of 10,000,000.00, a mean error of 0.01 Hz, and a standard deviation of this mean of 0.1 ppb. So, it holds the frequency close to a few parts per billion. This is about two orders of magnitude better than my function generator can achieve with an external reference.

Now turning to the schematic. The PWM averaging circuit can be seen feeding the adjustment voltage to the VCXO (which is the only device powered by the 3.3V source of the Nano). The circuit after the VCXO is used to square up the clipped sine wave of the VCXO output. This squared up, and buffered, signal is the 10 MHz square wave going not only to the counter but, to the external BNC connectors.  Two of the BNC connectors supply a TTL signal. Another is configured with 2 inverter gates and a 33Ω resistor to create a roughly 50Ω output. A fourth BNC connector supplies a buffered 1PPS signal for use by external devices.

Below the Nano is the GPS module. These can be found on the web for around $10 to $12 for the module and antenna. The PCB is shown in Figure 4 and Figure 5.

Figure 4 PCB of GPS disciplined 10 MHz source.

Figure 5 Image of GPS disciplined oscillator PCB connected in package.

In the middle of the schematic is the 32-bit counter (Figure 1). This 74LV8154 (~$1) actually has two 16-bit counters that can be configured to a 32-bit counter. The counter design allows for reading the 32 bits of counter in 4 individual bytes by addressing the bytes. This requires 4 address lines and I ran short on I/O on the Nano so a 74LS139 is used to translate 2 lines from the Nano into the 4 lines needed on the counter.

The LCD shown in Figure 6 is a standard I2C, 20 character by 4 line LCD. At the bottom of the schematic is a simple linear power supply using a 12V AC/DC adapter and regulating to 8V to power the Nano and 5V for various parts of the circuitry. I used a linear supply to assure a quiet Vcc plane. Lastly, there are 2 LEDs.  One green LED displays the 1PPS signal. A second, bi-color LED that is green when the system has locked tightly to the desired 10 MHz, red if not locked yet, and alternating red/green if the system is in holdover. Holdover is a state in which current settings are maintained during loss of the 1PPS signal. To detect loss of the 1PPS signal, I enlisted the watchdog timer (WDT).  Instead of using the WDT to detect bad executing code issues, I set the WDT timeout to 2 seconds and pet (reset) it inside the 1PPS interrupt routine. If the system doesn’t get a 1PPS interrupt, the WDT is not petted.

Figure 6 Completed GPS disciplined oscillator in package with LCD.

The code for the Nano was written in C using the Arduino IDE. The code is essentially driven by the 1PPS interrupt which quickly responds by locking in the 32-bit counter value (a function of the 74LV8154), clearing the counter, and setting a reset flag. You may have figured out that we are missing some part of the next count because we are capturing the counter register and then clearing it, allowing it to start counting from 0 again. (There is some finite time between the capture and the clear.) This is true, so the firmware compensates for this latency by adding an offset value to the raw counter value. It turns out there are 16 missing counts. This has been verified two ways: first, by measuring the time between the capture register signal and the clear register signal on an oscilloscope. Second, I ran a number of tests without adjusting the VCXO, using 1 second and 100 second captures which were adjusted by a latency offset value. The 1 second captures were repeated 100 times and added up. If the latency offset is correct, the summed up 1 second values (which now contain 100 latency offsets), should equal the 100 second captured reading (which contains 1 latency offset). In the operating code, the missing 16 count is added back in on every read of the counter.

The main loop in the code (the Arduino development system always runs the classic C “Main” as a loop) waits for an interrupt flag to be set and when set, it reads the value of the locked in registers of the 74LV8154. Next, it checks to see if it is a “reasonable” reading and not corrupted (differing from 10 MHz by more than 12 ppm). If it is bad it is tossed. If it is good, it processes the value in a small boxcar averager. This average is then used to adjust the PWM’s up or down as described above. It then decides what stage it is in. The first stage runs 1 second counts and adjusts the VCXO. This quickly gets us in the ballpark for PWM settings.  This stage runs for 20 seconds. The second stage takes 10 second readings of the count to fine-tune the PWM settings. This is set to run for 5 passes or, 50 seconds.  The last stage runs 100 second readings and is run continuously thereafter, making continual adjustments to hold the frequency as temperature or other parameters change the frequency.

In the last stage, LEDs are set as needed and statistics are taken after each reading. These statistics can be viewed on the LCD and multiple pages (Figure 7) are viewable using the circuit’s pushbutton which is debounced in the main loop. Statistics include instantaneous frequency, average frequency, current ppb error, ppb average error, ppb error standard deviation, number of good readings, number of bad readings, time of continuous lock, max frequency seen, minimum frequency seen, and PWM coarse and fine settings.

Figure 7 Statistics on LCD taken after each reading.

Complete project information for this can be found at, the open-source site: https: (or you can search for “DamianB2”).

Project information includes full KiCad project with schematic, PCB, and PCBA BOM.  Also included is a full assembly BOM, Arduino source code, 3D print files for the enclosure, link to 3D print files for the GPS module enclosure, artwork for the nameplates, various notes, etc.

A last thought: some of you may have noticed that, while designing the GPS disciplined oscillator (GPSDO), we have inadvertently created the essence of a pretty good frequency counter. We’ll investigate this in the next installment.

This article was originally published on EDN.

Damian Bonicatto is an experienced hands-on design engineer with extensive background in research, development, design, implementation and testing of innovative hardware and firmware across multiple industries. 


Leave a comment