Metronome is a proof-of-concept disciplining scheme for ultralow noise oscillators. It uses python to compare the oscillator to a cesium clock reference, then adjusts the tuning voltage accordingly to keep the frequency from drifting. It currently uses an HP 53132A Universal Counter as the phase detector and an Agilent E3646A DC Power Supply to drive the tuning voltage, but these could be replaced by smaller/cheaper components in the future. Metronome was developed for use in Bell Labs quantum communications research.
If the oscillator drifts from the reference frequency, the phase between them will change as a function of time. More specifically, if the phase is measured on a scale that wraps back around after one cycle (as opposed to continually increasing or decreasing without bound), it will be a sawtooth wave whose frequency is equal to the difference in frequencies between the oscillator and the reference. The disciplining algorithm used by Metronome involves measuring samples of the phase between the oscillator and the reference over a period of time. After these samples are collected, it uses a Fast Fourier Transform (FFT) to find the fundamental frequency of the phase oscillations. This directly gives the oscillator's frequency offset from the reference, which is then multiplied by the tuning slope (2.5 V/Hz based on the Wenzel HF ULN oscillator datasheet) to find how much the tuning voltage should be changed.
Note that this FFT method was chosen over a simple PID controller so as to minimize the phase noise added to the oscillator. The purpose was to discipline an ultra-low noise oscillator, so it is critical to make adjustments to the tuning voltage as infrequently as possible. The FFT offers an elegant way to average the signal over a long period of time to make infrequent updates with a fine resolution.
- Sample the phase delta over a period of time.
- First, choose the sampling rate. This determines the maximum frequency offset you can measure due to the Nyquist limit. For example, I chose 10 SPS which meant I could measure up to 5 Hz away from the reference frequency.
- Next, choose the number of samples. This should be a power of 2 to optimize the FFT. Assuming a fixed sampling rate, taking more samples leads to higher frequency resolution, but it also means more time in between updates to the tuning voltage. I chose to start with 2^6 samples, which takes about 6.4 seconds and gives a frequency resolution of 0.15 Hz. This allows earlier tuning of the oscillator at a fairly low resolution. After this, each tuning iteration increases the exponent by 1 until it reaches 2^14 samples, at which point all subsequent iterations use 2^14 samples. Having 2^14 samples gives a much better frequency resolution of about 0.0006 Hz, but it takes about 27 minutes to complete. Starting at a smaller number of samples and increasing it as the oscillator is disciplined allows it to act quickly in the beginning while also having a good long term frequency resolution.
- Take the FFT of the signal measured in step 1 and find the fundamental frequency. This is the offset frequency.
- In order to find the fundamental frequency, the assumption is made that the fundamental will have the largest amplitude out of all the frequencies that make up the signal. This is not necessarily the case for an arbitrary signal, but it is true for the types of signals that a phase detector will output.
- Find the tuning offset by multiplying the offset frequency by the tuning slope, which can be found on the oscillator datasheet. For example, the Wenzel HF ULN Oscillator has a tuning range of +/- 2E-7 * 10 MHz over +/- 5 V → tuning slope = 5 V / 2 Hz = 2.5 V/Hz.
- This tuning offset is actually just the magnitude of the tuning offset. It does not tell you which direction to go. On the first iteration, assume that the tuning offset is positive. On all subsequent iterations, compare the offset frequency to that of the previous iteration. If the offset frequency has gotten worse (larger in magnitude), it means the previously assumed sign is wrong. Change directions by flipping the sign of the tuning offset, then continue with the algorithm. Make sure to keep this new sign for subsequent iterations until it is wrong and needs to be flipped again.
- Divide the tuning offset by 2 and add it to the current tuning voltage. Dividing it by 2 decreases the risk of overshooting and allows the oscillator to approach 10 MHz instead of trying to jump all the way there in one shot.
- Repeat from step 1.
Metronome was written in python. It consists of four files: metronome.py, cowbell_io.py, cowbell_simulator.py, and sim_time.py.
metronome.pyis the main file that implements the algorithm outlined above. It defines the following functions:fund(signal): Thesignalinput is a numpy array of samples representing the phase measured over a period of time. This function performs the FFT on the signal and returns the fundamental frequency.samp(exp): This function repeatedly measures the phase and stores the measurements in a numpy array of length 2^exp. The sample rate is defined in this function to be 10 SPS. It returns the numpy array of samples.tune(exp): This function performs one iteration of the algorithm outlined above. It callsfund(samp(exp))to find the offset frequency, then updates the tuning voltage accordingly.
cowbell_io.pydefines functions for reading the phase and setting the tuning voltage. Currently, it usespyvisato talk to both the HP 53132A Universal Counter and the Agilent E3646A DC Power Supply. However, in a future version that uses smaller/cheaper components instead of these instruments, these functions would likely be rewritten to usespidevto interface with the ADC and the DAC. This file defines the following functions:tune(v): This function sets the tuning voltage to the inputv. It also makes sure that the voltage is limited to a certain range, either -5 V to 5 V or 0 V to 5 V depending on whether the DAC is bipolar or unipolar.phase(): This function reads the current phase between the oscillator and the reference. It makes sure the number is between -180 deg and 180 deg, then returns this value. If the phase was 0 deg to 360 deg, the FFT method wouldn't work because of the DC component of the signal (it would always measure the fundamental frequency as 0 Hz).
cowbell_simulator.pyimplements a simulated version of the Wenzel HF ULN Oscillator. It assumes a linear frequency drift, and its purpose is to be able to test the tuning algorithm without using a real oscillator. It defines the same functions ascowbell_io.py, as well as a few other functions. In order to use a simulated oscillator instead of a real one, thecowbell_iofunctions can be swapped out for thecowbell_simulatorones inside ofmetronome.py. The following functions are defined in this file:update_ptune(): Theptunevariable keeps track of the total phase that has accumulated as a result of the tuning voltage. This is the integral of the tuning frequency over time. Theupdate_ptune()function is called whenever the tuning voltage is changed. It multiplies the tuning frequency by the amount of time that has passed since the last time it was updated and adds this value to ptune.freq(): This function returns the current frequency of the oscillator. This should not be used by the algorithm inmetronome.pybecause a real-life oscillator does not have this function. It is useful in the simulation file because it lets you see whether the algorithm is working.adc(value, bits, range): This function simulates an analog to digital converter (ADC). It takes thevalueinput and rounds it to the nearest value that could come out of the ADC based on the number ofbitsand therange.phase(phase_detector = True): This function returns the current phase between the oscillator and the reference. Thephase_detectorinput controls whether the phase is in units of degrees or volts. It would be in volts if you are simulating a phase detector that outputs a voltage proportional to the phase difference instead of a universal counter which outputs the value in degrees. Additionally, ifphase_detectorisTrue, the phase output over time will be a triangle wave instead of a sawtooth wave because of how a phase detector works.dac(value, bits, range): This is similar to the ADC function, except it simulates the DAC instead. It makes sure the tuning voltage output is rounded to the nearest possible step based on therangeand number ofbits.tune(v): This function changes the tuning voltage of the simulated oscillator. First it callsupdate_ptune(), then it sets the new tuning voltage.
sim_time.pyimplements simulated time so that the simulated oscillator can be run much more quickly than real time. This assists with development because the sampling function must take a long time to collect all the samples in order to have sufficient frequency resolution. Being able to speed up time is very helpful for seeing bugs. The following functions are defined insim_time.py:time(): This function returns the current time. Depending on the value of the global variableSIM, it will either return the real time or the simulated time. It uses python'stimemodule to return the real time. For the simulated time, there is a variabletwhich keeps track of the number of ticks which have passed. The value of a tick is one second divided by thetimescalevariable. This means that by changing the value oftimescale, the simulation can be run at different speeds.increment(): This function adds 1 to the value oft. It is called inmetronome.pyafter each sample is taken in order to make the simulated time move forward.
There is also a Jupyter Notebook file called fft.ipynb. This is not used in the actual disciplining system, but it is helpful for visualizing the time domain and frequency domain graphs of signals as well as experimenting with different sample rates and numbers of samples to see how they affect the total sampling time and frequency resolution.
The python code runs on a Raspberry Pi, which interfaces with the other devices. Currently, these devices are an HP 53132A Universal Counter for phase measurement and an Agilent E3646A DC Power Supply for tuning voltage control. Both of these use an RS232 serial connection. The counter uses a straight-through cable, and the power supply uses a crossover cable.
These instruments work as a proof-of-concept, but they are very large and expensive. The next step would be to replace the counter with a phase detector such as this and an ADC, and to replace the power supply with a DAC. These components are all much smaller and cheaper, so they would be better in a final design. The DAC and ADC would interface with the Raspberry Pi using SPI instead of RS232. There was not enough time to get past the proof-of-concept stage, but swapping out these components would be the next step.
