-
-
Notifications
You must be signed in to change notification settings - Fork 99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New/alternative method for exact solutions for PLL and multisync ratios #79
Comments
Here are some code snippets for reference. Quick code for finding the different values, just a couple of lines of code and some examplesFast GCD AlgorithmI did some tests on an Uno to find the fastest, lowest memory GCD algorithm. Below is the conclusion so far, but it might even be able to go faster
and the lcm algorithm then is simple:
|
@maqifrnswa Thats really interesting. I am looking for sub-hz accuracy, at least to 1/10 Hz, maybe even 1/100Hz and have been struggling to get that except for very specific pre-calculated frequencies. Clock Builder Pro seems to calculate it easily, perhaps using the method you describe above - but the above is too complicated for me to follow yet. Is this something you are able to implement as a pull request? Thanks very much, Kevin |
Yes, I'd be happy to!
Would it be helpful if I write it as a function that returns the values for
the PLL and multisynth multiplier values? This way you can use it wherever
you need it in the code. I'm afraid of making a mistake in the integration
with the existing code.
It's actually simple when you see the code (like 3 or 4 lines for each
case). What I wrote is more of a detailed derivation explaining why it
works. I also optimized the gcd algorithm even further using a binary GCD
method optimized for instruction set on the AVR. Finding the values now is
super fast.
Sub 1/10 Hz accuracy on a si5351 sounds ambitious - but if ClockBuilder can
do it, we should be able to too!
|
Thanks Scott, yes that would be great, it could be incorporated into the different ways people use the SI5351a then. Currently we use code like this:
with fixed PLL frequencies and denominator, but that generates errors up to about 0.4hz. With a WSPR tone spacing of 1.46Hz, thats a 30% error! A friend has found different PLL and denominator values that happen to work pretty well for a single small frequency range - but it would be great to have it work for any frequency. FYI, we are using this to generate protocols like WSPR, FT8 and others to transmit from balloons that travel around the world, one example that is currently over South Korea: https://tracker.habhub.org/#!mt=roadmap&mz=4&qm=All&mc=35.02446,140.71971&q=BSS34 Thanks very much, |
That's awesome! I'm actually the advisor of a student group launching a HAB this spring. We're building a cubesat, so the HAB will just be testing some systems (and giving them experience). I want them to try a long term one next year! I didn't do a pull request, but here's the function and some demo code: You give the function your target freq and your oscillator: calcDiv1(desired_freq, osc_freq) Right now I just have it for one clock on one PLL (but can do more, but want other's opinion first), and it doesn't use R dividers so it's just the higher frequencies. They can be added too, but I kept it simple to get feedback first. |
Brilliant, thank you Scott, I will look to integrate that with a setFrequency routine to put that into the SI5351a and see how it goes. |
Awesome. It's currently limited to the frequencies without R dividers. I think it almost always finds even integer dividers, so it could work for MS6 and MS7 (but would have to be hardcoded to ensure it only finds even integer dividers).
Once integrated, I can see how you integrated it and add in something for "resolution" so that you can get 1/10th or 1/100th Hz resolution.
EDIT: another thing on to add to the list would be "fast" value calculating and setting. If you already set one of the clocks and you want to change the frequency quickly, you can keep your old MS divider and calculate the new PLL fractional divider. Faster computation and fewer I2C calls.
And I saw that your balloon came within 40 km of where I live on March 3rd. Maybe it'll make it back in a few days!
|
Checking out how the uSDX project and Orion WSPR pico balloon project do it, I see a need for a "fast" option that keeps the MS divider constant as much as possible. Both of those projects take approximations as well, this code almost never takes an approximation (I got lazy and just threw a catch all that never should happen). Speed wise, it's probably the same as the full gist, but that full gist changes both the PLL and the MS registers. This one doesn't change the MS stuff unless it has to, so less data is sent over I2C every time you change the clock. If you only care about 1 clock, you want to keep the MS registers unchanged and really only touch the PLL FB registers. This should improve stability and speed when scanning through lots of frequencies (since you are sending less data to the si5351) and almost completely avoid needing to touch the reset button. Also, this uses very little memory. No need for uint64s. call it like this: dividerVals_t divCalcFast(uint32_t desired_freq, uint32_t osc_freq, uint8_t dCurr) {
const uint32_t FVCO_MIN = 600000000, FVCO_MAX = 900000000;
dividerVals_t divider_vals = {0, 0, 0, 0, 0, 0, 0, 0};
uint32_t fvco = desired_freq * dCurr; // zero when d = 0, current fvco
if (fvco > FVCO_MAX || fvco < FVCO_MIN) {
dCurr = (FVCO_MIN + FVCO_MAX) / 2 / desired_freq; // set fvco to the mid point, for max tuning
//d = FVCO_MAX / desired_freq;
dCurr &= 0xFE; // this is the projected d
}
uint8_t d = dCurr; // this is our working value of d, either the current or projected d
// make sure the d works. First try with even integer d, then odd.
// If doesn't work, change d and try again.
do {
fvco = desired_freq * d;
ldiv_t xDivY = ldiv(fvco, osc_freq);
uint32_t gcdTemp = gcd(xDivY.rem, osc_freq);
divider_vals.FB_a = xDivY.quot;
divider_vals.FB_b = xDivY.rem / gcdTemp;
divider_vals.FB_c = osc_freq / gcdTemp;
divider_vals.FB_int = 0;
d = (dCurr - d < 0) ? d + 2 * (dCurr - d) : d + 2 * (dCurr - d) + 2; // find even d centered near mid point
} while (divider_vals.FB_c > 1048757UL && fvco > FVCO_MIN && fvco < FVCO_MAX );
// lazy catch all for now
if (divider_vals.FB_c > 1048757UL) {
fvco = desired_freq * dCurr;
ldiv_t xDivY = ldiv(fvco, osc_freq);
divider_vals.FB_a = xDivY.quot;
divider_vals.FB_b = xDivY.rem;
divider_vals.FB_c = osc_freq;
while (divider_vals.FB_c > 1048757UL) {
divider_vals.FB_b >>= 1;
divider_vals.FB_c >>= 1; // just keep halving until you hit a good value
}
divider_vals.FB_int = 0;
}
divider_vals.MS_a = d;
divider_vals.MS_b = 0;
divider_vals.MS_c = 1;
divider_vals.MS_int = !(0b1L & divider_vals.MS_a); // set true if even
return divider_vals;
} EDIT: this looks like a lot, and the gist looks like a lot - but only very little of the code is run each time (like 5-6 lines). It just handles every possible case. |
Thanks Scott, I am having a look at getting it working now - getting a click free, frequency error free routine is certainly a challenge! |
So, after a play I realise I currently have frequency as a uint64_t, representing CentiHz, to enable the requesting of 1.46hz tone spacing for WSPR, or 6.25Hz for FT8, or 1.736Hz for JT9. This and other research I have been doing I guess is telling me that my requirements are slightly more complicated than first stated, given the complexities of changing frequency in the SI5351a. I need to set a frequency that is about correct (plus or minus a few Hz is fine, the crystal and other factors will cause differences in absolute value), but then I need the ability to rapidly, smoothly, and accurately change frequency by between 1 and 3 x 1.46hz, between 1 and 7 x 6.25Hz or between 1 and 7 x 1.736Hz for WSPR, FT8 and JT9 respectively (and possibly other spacing for other protocols or glyphs). To achieve that, should we be choosing an initial frequency based on being able to change to the required subsequent tones rapidly, smoothly, and accurately? Perhaps pre-calculating the values for the subsequent tones, allowing a change of tone to be as simple as possible? Just as an example of the type of issues. My current routine (based on fixed FVCO and denominator), generates huge amount of spurious noise when changing frequency. Turning the clock off, changing frequency, then turning it back on again gets rid of a lot of that noise, but replaces it with a band wide clicking - you can see the two examples in this clip: https://imgur.com/a/mrWh3YX Thanks very much for all your work so far, and the investigations into how uSDX and Orion are doing it. Cheers |
Kevin - that makes sense, did you try the "divCalcFast()" function? That one might allow you to tune smoothly, but it's currently limited to 1 Hz resolution. If it can work smoothly with 1 Hz resolution, then we can increase resolution. From what I've read, the PLL reset is what makes the pop, and you don't need to do the PLL reset every time you change the divider settings. the Otherwise there's another function I can write that does precalculate it like you said. It can be something like: and return the common MS divider and FB dividers for each channel. |
Hi Scott, I didn't, I got to the 'how do I test this with out sub hz setting' point. I can test it without wspr and confirm the hz settings are equally spaced for say 4 x 1hz tones if that is a first step? The PLL reset does indeed make a pop, but so does turning the MS on/off, or turning the output on/off. Equally I guess changing frequency whilst still transmitting also generates some stray tones, but much harder to see that. I will look to test 4 x 1hz tones and confirm they are equally spread with the current proposed routine. |
Here you go! This takes the desired target frequency and channel steps (in units of .01 Hz) and returns the common MS divider for all the channels, and a PLL divider that if you increment the PLL b you get to increase one channel at a time. typedef struct {
uint8_t FB_a;
uint32_t FB_b, FB_c;
uint8_t MS_a;
uint32_t MS_b, MS_c;
bool FB_int, MS_int;
} dividerVals_t;
dividerVals_t divCalcChans(uint32_t desired_freq, uint32_t osc_freq, uint32_t channelSpacing) {
// freq units are in .01 Hz
const uint64_t FVCO_MIN = 60000000000ULL, FVCO_MAX = 90000000000ULL;
uint32_t Y = 1048575UL;
dividerVals_t divider_vals = {0, 0, 0, 0, 0, 0, 0, 0};
// A+B/C is MS feedback. PLL feedback is X/Y
// make channel spacing equal 1 PLL feedback incremenent:
// channel spacing*(A+B/C) = fosc*1/Y
// if PLL feedback is X/Y, and 1 channel is 1/Y PLL steps, what is X?
// uint32_t X = desired_freq/channelSpacing;
// ok so what is the initial multiplier?
uint32_t initPLLA = desired_freq/channelSpacing/Y;
if (initPLLA < 8) Y = Y/8*initPLLA; // make Y such that PLLA is >8
initPLLA = desired_freq/channelSpacing/Y; // now check that the PLLA is big enough to get 600 MHz
if (initPLLA < FVCO_MIN/osc_freq) Y = Y / (FVCO_MIN/osc_freq)*initPLLA;
// when x = 1 for channel spacing, (A+B/C) = fosc/ (channelspacing*Y)
do {
uint32_t denom = channelSpacing*Y;
divider_vals.MS_a = osc_freq/denom;
uint32_t rem = osc_freq%denom;
uint32_t gcdTemp = gcd(rem, denom);
divider_vals.MS_b = rem%denom/gcdTemp;
divider_vals.MS_c = denom/gcdTemp;
Y--;
} while (divider_vals.MS_c > 1048575UL);
Y++; // correct for extra --
uint32_t X = desired_freq/channelSpacing;
divider_vals.FB_a = X/Y;
divider_vals.FB_b = X%Y;
divider_vals.FB_c = Y;
// no gcds, we alread know that Y is < max it can be, and incrementing PLLB is by one changes channels
return divider_vals;
}
uint32_t gcd(uint32_t u, uint32_t v) {
int shift;
if (!u) return v; // if zero
if (!v) return u; // if zero
shift = __builtin_ctzl(u | v);
while ( !(u & 0b1L)) u >>= 1; // manual u >>= __buitin_ctzl(u)
while (v ) {
while ( !(v & 0b1L)) v >>= 1;
if (u > v) {
uint32_t t = v;
v = u;
u = t;
}
v = v - u;
}
return u << shift;
} If you put this in your setup(): Serial.begin(9600);
delay(1);
Serial.println("all for 27 MHz clock");
dividerVals_t results = {0, 0, 0, 0, 0, 0, 0, 0};
results = divCalcChans(700000000UL, 2700000000UL, 146);
Serial.println("7 MHz with 1.46 Hz channels if FB_b is incremented by 1");
Serial.println(results.FB_a);
Serial.println(results.FB_b);
Serial.println(results.FB_c);
Serial.println(results.FB_int);
Serial.println(results.MS_a);
Serial.println(results.MS_b);
Serial.println(results.MS_c);
Serial.println(results.MS_int); You get the following output:
Which corresponds to: Exactly, with no approximations. And all you have to do is change the PLL feedback registers that hold "b" to switch channels. |
So I got this far, you will see I have two set frequency routines included, the KW one works and produces a 14Mhz signal, but unfortunately the one using your routine does not. I am 100% sure it is something in my code stopping it working, but I cant see what at the moment:
|
The KW algorithm picks an intermediate frequency far from the ones that calcDivFast finds, so when switching from the two methods, you might need a PLL That said, maybe swap out the divCalcFast for the new function above that gives exact values for a b and c for PLL and MS dividers with 0.01 Hz resolution for a given channel spacing. Mathematically, it just works and uses values within the data sheet range - so the only reason I can think of why it wouldn't work is if a PLL reset is needed if the change from the previous settings is significant. |
Hi Scott, been looking at this again today, and thank you for your continued work on it. I tried an additional PLL reset but no change. I tried hard coding results with my manual 'results': '''dividerVals_t results = {33, 194180, 1048575, 64, 0, 1, 0 , 0}; and that works fine in SetFrequency, producing the expected 14Mhz tone. I then tried calling divCalcFast and passing in results.MS_a from the hard coded result above but that did not produce a visible frequency output :( So my final code at this point is here: (I need to learn how to use gist!) and outputs this:
I think I need to get this working to this point before trying the more complex next step... P.S. Happy to talk outside of Git if easier - kevin at unseen org |
Is your feature request related to a problem? Please describe.
The current method for finding the PLL and multisync ratios for register values are based on some hardcoded values that leads to (rounding) errors between the desired frequency and what gets set. Enclosed is a computationally cheap approach to find values for the dividers that (1) allow you to set the integer bit on the output divider and (2) give you the exact values needed for a given frequency. It works for one or two clocks on a PLL.
Describe the solution you'd like
Either update the functions the calculate the PLL and multisync values, or add a new function for "enhanced_accuracy_set_freq" or something like that.
The current method sets the PLL to 800 MHz then finds approximate values for the A+B/C output divider ratios to set the correct frequency by setting C=RFRAC_DENOM. That can work for many situations, but introduces error on the order of Vxtal/RFRAC_DENOM. For the same computational effort, you can find exact solutions. Below is a demonstration of how using a single clock on a PLL, then I'll show with 2 clocks per PLL.
Single Clock per PLL, exact solution, set integer divide bit on multisync output divider
Fundamentally, this is the equation that governs the 5351:
I just use x1/y1 instead of A +B/C for simplicity. x1,y1,x2, and y2 are all integers, and y1, y2 < 1,048,575. We're also told that we should try to make as many output dividers integers (even better if they are even integers so we can set the integer bit!) So let's rewrite it as:
Where A1 is the output divisor that we'll make sure is an even integer. Here's the pseudocode that is explained in more detail below:
Example
In this example, we'll make an output at Fout=7.1MHz using a Fxtal=27 MHz crystal.
A1= round down to the nearest even integer (900MHz/Fout)
. To do that, first pick your Fco. Let's say close to 900 MHz (Si does that for their clockbuilder pro, so we can too).y2 = Fxtal/COMMON_SCALING_FACTOR
whereCOMMON_SCALING_FACTOR
is any integer that goes into Fxtal and Fout an integer number of times. Doing that guarantees that y2 is an integer and that Fout*y2/Fxta=x2 is an integer. How do you find that? Two ways:COMMON_SCALING_FACTOR=10^something
such that you just drop out all those zeros you don't need. In our case.Fxtal = 27,000,000
andFout=7,100,000
, so makeCOMMON_SCALING_FACTOR=10,000
so you get two integers when you divide Fout and Fxtal by COMMON_SCALING_FACTOR:7,100,000/COMMON_SCALING_FACTOR=71
and27,000,000/COMMON_SCALING_FACTOR=270
. Theny2 = Fxtal/COMMON_SCALING_FACTOR=270
COMMON_SCALING_FACTOR
- which is also known as the greatest common denominator between the two. But it's slow.y2 = Fxtal/gcd(Fout,Fxtal)
where gcd is the greatest common denominator using tricks like Euclidian Expansion. In this case,gcd = 10,000
, just a coincidence that it's what we choose above.x2 = Fout*A1/(Fxtal/y2)=7.1e6*126/(27e6/270)=8946
You have to make sure that
y2<=1,048,757
. If it doesn't, see the next sectionSingle Clock per PLL, exact solution, fractional divide on multisync output (if you really want Hz level precision)
y2<=1,048,757
If a Fout was chosen such thaty2>1,048,757
(that happens if you want a lot of precision in your output frequency), you can either:y2 = 1,048,757
and accept the tiny error (on the order of 3 Hz)A1
. In that case, you don't need to find a COMMON_SCALING_FACTOR anymore. Instead just do the following:where INTEGER_FACTOR1 is any integer that also makes y1 an integer such that y1 < 1,048,757 and is wholly divisible into 900 MHz. That makes it easy - you can pick
10^something
. And you can do the same thing with the feedback equationwhere INTEGER_FACTOR2 is any integer that also makes y2 an integer such that y2 < 1,048,757 and is wholly divisible into 900 MHz. That makes it easy - you can pick
10^something
.Multiple clocks per PLL, exact solution, at least 1 integer divide multisync output (maybe both if you are lucky!)
Now we have 2 Fouts (you can extend this to as many Fouts per PLL that you'd like, follow the same exact proceedure):
We'll guarantee that at least 1 output can set the multisynth even integer divide bit (where Fout0 < Fout1)
We go through the same process as above
Everything integer multiples
Maybe this can be an extra function, but we can say you want everything integer multiples (PLL feedback divider and multisync dividers all integers). Here's the required condition:
For this to work, you need Fvco to be a integer factor times the least common multiple, and between 600-900 MHz.
They are a little limiting, but possible. Here are some examples
Describe alternatives you've considered
A bunch, but mathematically this is the purest and computationally the simplest.
The text was updated successfully, but these errors were encountered: