Skip to content
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

Open
maqifrnswa opened this issue Feb 22, 2021 · 16 comments
Open

Comments

@maqifrnswa
Copy link

maqifrnswa commented Feb 22, 2021

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:

600 MHz < Fout*x1/y1 = Fxtal*x2/y2 < 900 MHz

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:

600 MHz < Fout*A1 = Fxtal*x2/y2 < 900 MHz

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:

A1 = floor(900e6/Fout0)

COMMON_SCALING_FACTOR=gcd(Fout, Fxtal) // greatest common denominator
// see text below for a computationally faster way, you don't really need the gcd, any common factor will do.

y2 = min(Fxtal/COMMON_SCALING_FACTOR, 1048757)
// The above accounts for requesting Fouts such that y1,y2 > 1048757. Only happens when Fout's precision is on the order
// of 3 Hz. So it should rarely happen.
// However, if you want the exact solution: follow steps below for "fractional divide" below.

x2 = Fout*A1*y2/Fxtal

PLL Feedback Equation:
A+B/C
A=floor(x2, y2)
B = x2 % y2
C = y2

Multisynth Output Divider
A+B/C
A = A1
B = 0
C = 1
You can set the even integer divide bit!

Example

In this example, we'll make an output at Fout=7.1MHz using a Fxtal=27 MHz crystal.

  1. Find A1. 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).
A1 < 900MHz/7.1MHz
A1 < 126.76
A1 = 126
  1. Next, we find y2 using y2 = Fxtal/COMMON_SCALING_FACTOR where COMMON_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:
    1. Fast way: make 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 and Fout=7,100,000, so make COMMON_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 and 27,000,000/COMMON_SCALING_FACTOR=270. Then y2 = Fxtal/COMMON_SCALING_FACTOR=270
    2. Slowest computationally, but you can exactly find the maximum 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.
  2. Now you can find x2 = Fout*A1/(Fxtal/y2)=7.1e6*126/(27e6/270)=8946
  3. Finally, you care write the exact solution!
PLL feedback ration values:
A+B/C = x1/y1 = A1 = 126
A = 126 (we can set the integer divide bit!)
B = 0
C = 1

Multisynth equation
A+B/C = x2/y2 = 8946/270
A = floor(8946, 270) = 31
B = 8946 % 270 = 36
C = 270
A+B/C = 31 + 36/270

You have to make sure that y2<=1,048,757. If it doesn't, see the next section

Single Clock per PLL, exact solution, fractional divide on multisync output (if you really want Hz level precision)

  1. You have to make sure that y2<=1,048,757 If a Fout was chosen such that y2>1,048,757 (that happens if you want a lot of precision in your output frequency), you can either:
    1. just go and put a hard cap on y2 = 1,048,757 and accept the tiny error (on the order of 3 Hz)
    2. or you get an exact solution, but lose out on using the scaling multiplier for A1. In that case, you don't need to find a COMMON_SCALING_FACTOR anymore. Instead just do the following:
y1 = Fout/INTEGER_FACTOR1
x1 = 900e6*/INTEGER_FACTOR1  <-- needs to be an integer. If you choose INTEGER_FACTOR1 to be 10^something, this will be an integer too!
Multisynth output equation:
A+B/C
A = floor(x1, y1)
B = x1 % y1
C = y1

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 equation

y2 = Fxctl / INTEGER_FACTOR2
x2 = 900e6*/INTEGER_FACTOR2
PLL feedback equation:
A+B/C
A =floor(x2, y2)
B = x2 % y2
C = y1

where 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):

600 MHz <  Fout0*x0/y0 = Fout1*x1/y1 = Fxtal*x2/y2 < 900 MHz

We'll guarantee that at least 1 output can set the multisynth even integer divide bit (where Fout0 < Fout1)

600 MHz <  Fout0*A0 = Fout1*x1/y1 = Fxtal*x2/y2 < 900 MHz

We go through the same process as above

A0 = floor(900e6/Fout0)

Find the COMMON_SCALING_FACTOR between Fout0, Fout1, Fxtal either by:
SLOW method: COMMON_SCALING_FACTOR=gcd(gcd(Fout0,Fout1),Fxtal)
FAST method: COMMON_SCALING_FACTOR = 10^something that gets us the right significant figures
such that COMMON_SCALING_FACTOR wholy divides into Fout0, Fout1, and Fxtal

y1 = min(Fout1/COMMON_SCALING_FACTOR, 1048757)
y2 = min(Fxtal/COMMON_SCALING_FACTOR, 1048757)
The above accounts for requesting Fouts such that y1,y2 > 1048757. If you need precision, follow the above steps using INTEGER_FACTORX for each Fout and Fxtal instead of a single COMMON_SCALING_FACTOR.

x1 = Fout0*A0*y1/Fout1
x2 = Fout0*A0*y2/Fxtal

PLL Feedback Equation:
A+B/C
A=floor(x2, y2)
B = x2 % y2
C = y2

Multisynth 0 Output Divider
A+B/C
A = A0
B = 0
C = 1
You can set the even integer divide bit!

Multisynth 1 output Divider
A+B/C
A= floor(x1, y1)
B = x1 % y1
C = y1
If B = 0, you can set the even integer divide bit here too! (basically, it's a harmonic or copy of Fout0)

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:

Fout0*A0=Fout1*A1=Fxtal*A2=Fvco

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

One output, 1 PLL:
Fout0 = 7 MHz
Fxtal = 25 Mhz
lcm(Fout, Fxtal) = 175 MHz
Fvco = ceil(600 MHz / lcm(Fout, Fxtal) )* 600 MHz = 700 MHz
A0 = 700/7 = 100
A2 = 700/25 = 28

One output, 1 PLL:
Fout0 = 7 MHz
Fxtal = 27 Mhz
lcm(Fout0, Fxtal) = 189 MHz
Fvco = ceil(600 MHz / lcm(Fout, Fxtal) )* 600 MHz = 756 MHz
A0 = 756/7 = 108
A2 = 756/27 = 28

Two outputs, 1 PLL:
Fout0 = 6 MHz
Fout1 = 4 Mhz
Fxtal = 25 Mhz
lcm( lcm(Fout0, Fxtal), Fout1) = 300MHz
Fvco = ceil(600 MHz / lcm(Fout, Fxtal) )* 600 MHz = 600 MHz
A0 =600/6 = 100
A1 = 600/4 = 150
A2 = 600/25 = 24

Two outputs, 1 PLL:
Fout0 = 6 MHz
Fout1 = 7 Mhz
Fxtal = 25 Mhz
lcm( lcm(Fout0, Fxtal), Fout1) = 1.05 GHz
Fvco = ceil(600 MHz / lcm(Fout, Fxtal) )* 600 MHz = 1.05 GHz
Not possible! The fastest we can go is 900 Mhz!

Describe alternatives you've considered
A bunch, but mathematically this is the purest and computationally the simplest.

@maqifrnswa
Copy link
Author

maqifrnswa commented Feb 24, 2021

Here are some code snippets for reference.

Quick code for finding the different values, just a couple of lines of code and some examples

https://www.reddit.com/r/amateurradio/comments/lpdfpx/tutorial_how_to_find_the_exact_divider_ratios_fo/

Fast GCD Algorithm

I 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
https://forum.arduino.cc/index.php?topic=729334.0

unsigned long gcd_ctz_manual(unsigned long u, unsigned long v) {
  int shift;
  if (u == 0) return v;
  if (v == 0) return u;
  shift = __builtin_ctzl(u | v);
  
  while ( !(u & 0b1L)) u >>= 1;
  while (v != 0) {
    while ( !(v & 0b1L)) v >>= 1;
    if (u > v) {
      unsigned long t = v;
      v = u;
      u = t;
    }
    v = v - u;
  }
  return u << shift;
}

and the lcm algorithm then is simple:

unsigned long lcm(unsigned long a, unsigned long b) {
  return a / gcd_ctz_manual(a, b) * b;
}

@KevWal
Copy link

KevWal commented Mar 7, 2021

@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

@maqifrnswa
Copy link
Author

maqifrnswa commented Mar 10, 2021 via email

@KevWal
Copy link

KevWal commented Mar 10, 2021

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:

 rDiv = SI_R_DIV_1;
 Divider = 90000000000ULL / frequency;// Calculate the division ratio. 900MHz is the maximum internal (expressed as deciHz)
 pllFreq = Divider * frequency; // Calculate the pllFrequency:
 mult = pllFreq / (FactoryData.RefFreq * 100UL); // Determine the multiplier to
 l = pllFreq % (FactoryData.RefFreq * 100UL); // It has three parts:
 f = l; // mult is an integer that must be in the range 15..90
 f *= 1048575; // num and denom are the fractional parts, the numerator and denominator
 f /= FactoryData.RefFreq; // each is 20 bits (range 0..1048575)
 num = f; // the actual multiplier is mult + num / denom
 denom = 1048575; // For simplicity we set the denominator to the maximum 1048575
 num = num / 100;

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,
kevin@unseen.org

@maqifrnswa
Copy link
Author

maqifrnswa commented Mar 10, 2021

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:
https://gist.github.com/maqifrnswa/54a953612134cf390b560a04b35f2a3d

You give the function your target freq and your oscillator: calcDiv1(desired_freq, osc_freq)
It returns a struct containing all the info for the PLL and MS registers (a,b,c,int_bit). The code is just an arduino sketch that shows how it would work for finding exact values for 7000001 Hz, 7.1 MHz, and 7 MHz. The default is 1 Hz resolution - but you can get better resolution if you just change the units (e.g., multiply every frequency by 10 or 100 in the function). It doesn't do it yet inside the function, but it can. We can add another argument to the function for "units" or "resolution."

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.

@KevWal
Copy link

KevWal commented Mar 11, 2021

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.

@maqifrnswa
Copy link
Author

maqifrnswa commented Mar 11, 2021 via email

@maqifrnswa
Copy link
Author

maqifrnswa commented Mar 12, 2021

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:
dividerValues = divCatcFast(desired_freq, osc_frew, dCurr); where dCurr is the current MS divider value (a). If dCurr is zero, it will calculate one that is in the mid of the PLL range so you can have tunability both up and down from there without changing the MS_a. You might have to, that's what the checks are. But you can then use the new MS_a in future calcs.

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.

@KevWal
Copy link

KevWal commented Mar 13, 2021

Thanks Scott, I am having a look at getting it working now - getting a click free, frequency error free routine is certainly a challenge!

@KevWal
Copy link

KevWal commented Mar 13, 2021

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

@maqifrnswa
Copy link
Author

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 divCalcFast() function is designed to be smoother when tuning.

Otherwise there's another function I can write that does precalculate it like you said. It can be something like:
channelDiv(startFreq, numberChannels, channelSpacing)

and return the common MS divider and FB dividers for each channel.

@KevWal
Copy link

KevWal commented Mar 17, 2021

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.

@maqifrnswa
Copy link
Author

maqifrnswa commented Mar 17, 2021

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.
The channel spacing is exact, however the fundamental frequency might be off by sub hertz. I think this is OK for WSPR and similar digital modes where spacing matters more than the absolute frequency.

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:

7 MHz with 1.46 Hz channels if FB_b is incremented by 1
22
76070
214475
0
86
141038
626267
0

Which corresponds to:
Channel 0: 27000000*(22+76070/214475)/(86+ 141038/626267) = 6999999.20 Hz
Channel 1: 27000000*(22+76071/214475)/(86+ 141038/626267) = 7000000.66 Hz
Channel 2: 27000000*(22+76072/214475)/(86+ 141038/626267) = 7000002.12 Hz
Channel 3: 27000000*(22+76073/214475)/(86+ 141038/626267) = 7000003.58 Hz

Exactly, with no approximations. And all you have to do is change the PLL feedback registers that hold "b" to switch channels.

@KevWal
Copy link

KevWal commented Mar 17, 2021

I will look to test 4 x 1hz tones and confirm they are equally spread with the current proposed routine.

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:

#include <avr/wdt.h>  // Watchdog Timer functions

#include <SoftwareSerialMC_KW.h>
SoftwareSerial Serial1(7, 6); // RX, TX

#include <MemoryUsage.h> // Check memory avaliable for debuging

//Constants
#define I2C_START 0x08
#define I2C_START_RPT 0x10
#define I2C_SLA_W_ACK 0x18
#define I2C_SLA_R_ACK 0x40
#define I2C_DATA_ACK 0x28
#define I2C_WRITE 0b11000000
#define I2C_READ 0b11000001

// Register definitions
#define SI_CLK_OEB 3
#define SI_OEB_MASK 9
#define SI_CLK0_CONTROL 16
#define SI_CLK1_CONTROL 17
#define SI_CLK2_CONTROL 18
#define SI_SYNTH_PLL_A 26
#define SI_SYNTH_PLL_B 34
#define SI_SYNTH_MS_0 42
#define SI_SYNTH_MS_1 50
#define SI_SYNTH_MS_2 58
#define SI_PLL_RESET 177

// Register 3. Output Enable Control definitions
#define SI_CLK0_OEB 0b00000001
#define SI_CLK1_OEB 0b00000010
#define SI_CLK2_OEB 0b00000100

#define SI_CLK_SRC_PLL_A 0b00000000
#define SI_CLK_SRC_PLL_B 0b00100000

#define SI5_POWER A2

uint8_t data;

typedef struct {
  uint32_t FB_a, FB_b, FB_c, MS_a, MS_b, MS_c;
  bool FB_int, MS_int;
} dividerVals_t;


uint8_t i2cStart()
{
  // Enable internal pullup resistors
  digitalWrite(SDA, 1);
  digitalWrite(SCL, 1);
  
  TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN); // Set TWINT, TWSTA (Start) and TWEN (TwoWireEnable) Bits in the TwoWireControlRegister
  while (!(TWCR & (1 << TWINT))) ; // Wait until TWINT is set
  return (TWSR & 0xF8);
}

void i2cStop()
{
  TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
  while ((TWCR & (1 << TWSTO))) ;
}

uint8_t i2cByteSend(uint8_t data)
{ 
  TWDR = data;
  TWCR = (1 << TWINT) | (1 << TWEN);
  while (!(TWCR & (1 << TWINT))) ;
  return (TWSR & 0xF8);
}

uint8_t i2cByteRead()
{ 
  TWCR = (1 << TWINT) | (1 << TWEN);
  while (!(TWCR & (1 << TWINT))) ;
  return (TWDR);
}

uint8_t i2cSendRegister(uint8_t reg, uint8_t data)
{
  uint8_t stts;
  
  stts = i2cStart();
  if (stts != I2C_START) return 1;

  stts = i2cByteSend(I2C_WRITE);
  if (stts != I2C_SLA_W_ACK) return 2;

  stts = i2cByteSend(reg);
  if (stts != I2C_DATA_ACK) return 3;

  stts = i2cByteSend(data);
  if (stts != I2C_DATA_ACK) return 4;

  i2cStop();
  return 0;
}

uint8_t i2cReadRegister(uint8_t reg, uint8_t *data)
{
  uint8_t stts;

  stts = i2cStart();
  if (stts != I2C_START) return 1;

  stts = i2cByteSend(I2C_WRITE);
  if (stts != I2C_SLA_W_ACK) return 2;

  stts = i2cByteSend(reg);
  if (stts != I2C_DATA_ACK) return 3;

  stts = i2cStart();
  if (stts != I2C_START_RPT) return 4;

  stts = i2cByteSend(I2C_READ);
  if (stts != I2C_SLA_R_ACK) return 5;

  *data = i2cByteRead();

  i2cStop();
  return 0;
}

// Init TWI (I2C)
void i2cInit()
{
  TWBR = 0xB8; // TwoWireBitRate register (0xB8 = 184 default) 92 = 40khz I think?
  TWSR = 0; // TWI Status Register, Clear status codes and set prescaler to 0 which equals /1
  TWDR = 0xFF; // TWI Data Register
  //PRR = 0; // Power Reduction Register, 0 = start all modules, 
}


// Blink the LED
void blink(uint16_t ms, uint8_t times) {
  for (int i = 0; i < times; i++) {
    analogWrite(A1, 250); //150
    delay(ms/2);
    analogWrite(A1, 0);
    delay(ms*2);
  }
}


// Reboot
void(* resetFunc) (void) = 0; //declare reset function @ address 0


uint32_t ceilDivUL(uint32_t a, uint32_t b) {
  // Performs ceil(a/b) when a and b are unsigned long ints
  return (a - 1) / b + 1;
}

//uint32_t lcm(uint32_t a, uint32_t b) {
//  return a/gcd(a, b)*b;
//}


uint32_t gcd(uint32_t u, uint32_t v) {
  int shift;
  if (u == 0) return v;
  if (v == 0) return u;
  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;
}

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;
}


void SetFrequency (dividerVals_t results) 
{ 
  Serial1.print("  FB_a "); Serial1.println(results.FB_a);
  Serial1.print("  FB_b "); Serial1.println(results.FB_b);
  Serial1.print("  FB_c "); Serial1.println(results.FB_c);
  Serial1.print("  FB_int "); Serial1.println(results.FB_int);
  Serial1.print("  MS_a "); Serial1.println(results.MS_a);
  Serial1.print("  MS_b "); Serial1.println(results.MS_b);
  Serial1.print("  MS_c "); Serial1.println(results.MS_c);
  Serial1.print("  MS_int "); Serial1.println(results.MS_int);

  unsigned long MSNA_P1;               // Si5351a Feedback Multisynth register MSNA_P1
  unsigned long MSNA_P2;               // Si5351a Feedback Multisynth register MSNA_P2
  unsigned long MSNA_P3;               // Si5351a Feedback Multisynth register MSNA_P3

  unsigned long MS0_P1;                // Si5351a Output Divider register MS0_P1
  unsigned long MS0_P2;                // Si5351a Output Divider register MS0_P1
  unsigned long MS0_P3;                // Si5351a Output Divider register MS0_P1
    
  // Assume only use PLL A
  // MSNA_P1[17:0] = 128 * FB_a + Floor(128*(FB_b/FB_c))-512
  // MSNA_P2[19:0] = 128 * FB_b - FB_c * Floor(128*(FB_b/FB_c))
  // MSNA_P3[19:0] = FB_c
  // FBA_INT = FB_int // Integer mode?

  //MSNA_P1 = 128 * results.FB_a + floor(128 * (results.FB_b/results.FB_c)) - 512;
  //MSNA_P2 = 128 * results.FB_b - results.FB_c * floor(128 * (results.FB_b/results.FB_c));
  //MSNA_P3 = results.FB_c;

  MSNA_P1 = (uint32_t)(128 * ((float)results.FB_b / (float)results.FB_c));
  MSNA_P1 = (uint32_t)(128 * (uint32_t)(results.FB_a) + MSNA_P1 - 512);
  MSNA_P2 = (uint32_t)(128 * ((float)results.FB_b / (float)results.FB_c));
  MSNA_P2 = (uint32_t)(128 * results.FB_b - results.FB_c * MSNA_P2);
  MSNA_P3 = results.FB_c;

  i2cSendRegister (26, (MSNA_P3 & 0x0000FF00) >> 8);   // Bits [15:8] of MSNA_P3 in register 26
  i2cSendRegister (27, MSNA_P3 & 0x000000FF);            // Bits [7:0] of MSNA_P3 in register 27
  i2cSendRegister (28, (MSNA_P1 & 0x00030000) >> 16); // Bits [17:16] of MSNA_P1 in bits [1:0] of register 28
  i2cSendRegister (29, (MSNA_P1 & 0x0000FF00) >> 8);   // Bits [15:8] of MSNA_P1 in register 29
  i2cSendRegister (30, MSNA_P1 & 0x000000FF);            // Bits [7:0] of MSNA_P1 in register 30
  i2cSendRegister (31, ((MSNA_P3 & 0x000F0000) >> 12) | ((MSNA_P2 & 0x000F0000) >> 16)); // Parts of MSNA_P3 and MSNA_P1 in 31
  i2cSendRegister (32, (MSNA_P2 & 0x0000FF00) >> 8);   // Bits [15:8] of MSNA_P2 in register 32
  i2cSendRegister (33, MSNA_P2 & 0x000000FF);            // Bits [7:0] of MSNA_P2 in register 33
  
  // Assume only use MS 0
  // MS0_SRC = 0 // Connect PLL A output to MultiSynth 0 input
  // MS0_P1[17:0] = 128 * MS_a + Floor(128*(MS_b/MS_c))-512
  // MS0_P2[19:0] = 128 * MS_b - MS_c * Floor(128*(MS_b/MS_c))
  // MS0_P3[19:0] = MS_c
  // MS0_INT = MS_int // Even Integer mode?

  //MS0_P1 = 128 * results.MS_a + floor(128 * (results.MS_b/results.MS_c)) - 512;
  //MS0_P2 = 128 * results.MS_b - results.MS_c * floor(128 * (results.MS_b/results.MS_c));
  //MS0_P3 = results.MS_c;

  MS0_P1 = (uint32_t)(128 * ((float)results.MS_b / (float)results.MS_c));
  MS0_P1 = (uint32_t)(128 * (uint32_t)(results.MS_a) + MS0_P1 - 512);
  MS0_P2 = (uint32_t)(128 * ((float)results.MS_b / (float)results.MS_c));
  MS0_P2 = (uint32_t)(128 * results.MS_b - results.MS_c * MS0_P2);
  MS0_P3 = results.MS_c;

  i2cSendRegister(42, (MS0_P3 & 0x0000FF00) >> 8);  // Bits [15:8] of MS0_P3 in register 42
  i2cSendRegister(43, (MS0_P3 & 0x000000FF));      // Bits [7:0] of MS0_P3 in register 43
    // TODO Reg 44 MS0_DIVBY4 in [3:2]
  i2cSendRegister(44, (MS0_P1 & 0x00030000) >> 16);// Bits [17:16] of MS0_P1 in bits [1:0]  MS0_DIVBY4 in [3:2] and R0_DIV in [7:4]
  i2cSendRegister(45, (MS0_P1 & 0x0000FF00) >> 8); // Bits [15:8] of MS0_P1 in register 45
  i2cSendRegister(46, (MS0_P1 & 0x000000FF));      // Bits [7:0] of MS0_P1 in register 46  
  i2cSendRegister(47, ((MS0_P3 & 0x000F0000) >> 12) | ((MS0_P2 & 0x000F0000) >> 16)); // Bits [19:16] of MS0_P2 in bits [7:4] and [19:16] of MS0_P3 in [3:0]
  i2cSendRegister(48, (MS0_P2 & 0x0000FF00) >> 8); // Bits [15:8] of MS0_P2
  i2cSendRegister(49, (MS0_P2 & 0x000000FF));      // Bits [7:0] of MS0_P2

  // Register 16. CLK0 Control 
  // ToDo Set MultiSynth 0 Integer mode when we can, 
  // 0b00001111, 0x0F, Powered up:Yes:0, Integer mode:No:0, PLL:A:0, Inverted:Not:0, Source:MS0:11, Drive:8ma:11
  i2cSendRegister(SI_CLK0_CONTROL, 0x4F); 
}


// Set up specified PLL with mult, num and denom
// mult is 15..90
// num is 0..1,048,575 (0xFFFFF)
// denom is 0..1,048,575 (0xFFFFF)
void setupPLL(uint8_t pll, uint8_t mult, uint32_t num, uint32_t denom)
{
  uint32_t P1; // PLL config register P1
  uint32_t P2; // PLL config register P2
  uint32_t P3; // PLL config register P3

  //DEBUG_PRINT(F("setupPLL(): "));DEBUG_PRINTLN(mu_freeRam()); DEBUG_FLUSH();

  P1 = (uint32_t)(128 * ((float)num / (float)denom));
  P1 = (uint32_t)(128 * (uint32_t)(mult) + P1 - 512);
  P2 = (uint32_t)(128 * ((float)num / (float)denom));
  P2 = (uint32_t)(128 * num - denom * P2);
  P3 = denom;

  i2cSendRegister(pll + 0, (P3 & 0x0000FF00) >> 8);
  i2cSendRegister(pll + 1, (P3 & 0x000000FF));
  i2cSendRegister(pll + 2, (P1 & 0x00030000) >> 16);
  i2cSendRegister(pll + 3, (P1 & 0x0000FF00) >> 8);
  i2cSendRegister(pll + 4, (P1 & 0x000000FF));
  i2cSendRegister(pll + 5, ((P3 & 0x000F0000) >> 12) | ((P2 & 0x000F0000) >> 16));
  i2cSendRegister(pll + 6, (P2 & 0x0000FF00) >> 8);
  i2cSendRegister(pll + 7, (P2 & 0x000000FF));
}


// Set up MultiSynth with integer Divider and R Divider
// R Divider is the bit value which is OR'ed onto the appropriate
// register, it is a #define in si5351a.h
void setupMultisynth(uint8_t synth, uint32_t Divider, uint8_t rDiv)
{
  uint32_t P1; // Synth config register P1
  uint32_t P2; // Synth config register P2
  uint32_t P3; // Synth config register P3

  //DEBUG_PRINT(F("setupMultisynth(): ")); DEBUG_PRINTLN(mu_freeRam()); DEBUG_FLUSH();

  P1 = 128 * Divider - 512;
  P2 = 0; // P2 = 0, P3 = 1 forces an integer value for the Divider
  P3 = 1;

  i2cSendRegister(synth + 0, (P3 & 0x0000FF00) >> 8);
  i2cSendRegister(synth + 1, (P3 & 0x000000FF));
  i2cSendRegister(synth + 2, ((P1 & 0x00030000) >> 16) | rDiv);
  i2cSendRegister(synth + 3, (P1 & 0x0000FF00) >> 8);
  i2cSendRegister(synth + 4, (P1 & 0x000000FF));
  i2cSendRegister(synth + 5, ((P3 & 0x000F0000) >> 12) | ((P2 &
                  0x000F0000) >> 16));
  i2cSendRegister(synth + 6, (P2 & 0x0000FF00) >> 8);
  i2cSendRegister(synth + 7, (P2 & 0x000000FF));
}

void SetFrequencyKW(uint64_t frequency) // Frequency is in centiHz
{
  uint64_t pllFreq;
  uint32_t l;
  float f;
  uint8_t mult;
  uint32_t num;
  uint32_t denom;
  uint32_t Divider;

    // Divider = 90000000000ULL / frequency; // Calculate the division ratio. 600 to 900MHz is the maximum internal (deciHz)
    // Divider = 77000000000ULL / frequency; // New value from Eduard
    // Divider = 60000000000ULL / frequency; // New value from Kev
    Divider = 30000000000ULL / frequency; // New value from Kev
    pllFreq = Divider * frequency; // Calculate the pllFrequency:
    mult = pllFreq / ((27000000UL) * 100UL); // Determine the multiplier
    l = pllFreq % ((27000000UL) * 100UL); // It has three parts:
    f = l; // mult is an integer that must be in the range 15..90
    // f *= 1048575; // num and denom are the fractional parts, the numerator and denominator
    // f *= 1027000; // New value from Eduard
    //f *= 880100; // New value from Kev
    f *= 924000; // New value from Kev
    f /= (27000000UL); // each is 20 bits (range 0..1048575)
    num = f; // the actual multiplier is mult + num / denom
    // denom = 1048575; // For simplicity we set the denominator to the maximum 1048575
    // denom = 1027000; // New value from Eduard
    // denom = 880100; // New value from Kev
    denom = 924000; // New value from Kev
    num = num / 100;

  setupPLL(SI_SYNTH_PLL_A, mult, num, denom);
 
  // Set up MultiSynth Divider 0, with the calculated Divider.
  setupMultisynth(SI_SYNTH_MS_0, Divider, SI_R_DIV_1);

  i2cSendRegister(SI_CLK0_CONTROL, 0x4F);

}


void setup()
{
  dividerVals_t results = {0, 0, 0, 0, 0, 0, 0, 0};
  
  wdt_disable();
  
  pinMode(A1, OUTPUT);
  blink(400,3);

  Serial1.begin(57600);
  Serial1.println(F("START"));
  delay(3000);

  pinMode(SI5_POWER, OUTPUT);
  digitalWrite(SI5_POWER, HIGH);

  delay(20);                          // Max 10ms power up time
  i2cInit();                          // Initialise i2c for the SI5351a
  Serial1.println(F("si5351a powered on and I2C Initialised."));
  delay(5000);

  // Reg 17, 18. CLK1 & 2 Control 
  // 0b10001111, 0x8F, Powered up:No:1, Integer mode:No:0, PLL:A:0, Inverted:Not:0, Source:MS0:11, Drive:8ma:11
  i2cSendRegister(SI_CLK1_CONTROL, 0x8F);
  i2cSendRegister(SI_CLK2_CONTROL, 0x8F);

  // Read status registers
  i2cSendRegister(1, 0); // reset sticky bits
  Serial1.print(F("  Reg 0: ")); Serial1.print(i2cReadRegister(0, &data)); Serial1.print(F(" ")); Serial1.print(data);
  Serial1.print(F("  Reg 1: ")); Serial1.print(i2cReadRegister(1, &data)); Serial1.print(F(" ")); Serial1.print(data);
  Serial1.print(F("  Reg 2: ")); Serial1.print(i2cReadRegister(2, &data)); Serial1.print(F(" ")); Serial1.println(data);
  
  Serial1.println(F("Set Initial frequency and reset PLL's"));
  results = divCalcFast(14000000UL, 27000000UL, 0); // (desired, osc_clock, current MS divider value a)
  //SetFrequency(results);
  SetFrequencyKW(1400000000);
  i2cSendRegister(SI_PLL_RESET, 0xA0); // 0xA0 resets both PLLs
  delay(5000);

  // Read status registers
  i2cSendRegister(1, 0); // reset sticky bits
  Serial1.print(F("  Reg 0: ")); Serial1.print(i2cReadRegister(0, &data)); Serial1.print(F(" ")); Serial1.print(data);
  Serial1.print(F("  Reg 1: ")); Serial1.print(i2cReadRegister(1, &data)); Serial1.print(F(" ")); Serial1.print(data);
  Serial1.print(F("  Reg 2: ")); Serial1.print(i2cReadRegister(2, &data)); Serial1.print(F(" ")); Serial1.println(data);

  Serial1.println(F("First Tone 14097075UL"));
  results = divCalcFast(14097075UL, 27000000UL, 0); // (desired, osc_clock, current MS divider value a)
  SetFrequency(results);
  delay(5000);

  Serial1.println(F("Second Tone 14097076UL"));
  results = divCalcFast(14097076UL, 27000000UL, results.MS_a); // (desired, osc_clock, current MS divider value a)
  SetFrequency(results);
  delay(5000);

  Serial1.println(F("Third Tone 14097077UL"));
  results = divCalcFast(14097077UL, 27000000UL, results.MS_a); // (desired, osc_clock, current MS divider value a)
  SetFrequency(results);
  delay(5000);

  Serial1.println(F("Fourth Tone 14097078UL"));
  results = divCalcFast(14097078UL, 27000000UL, results.MS_a); // (desired, osc_clock, current MS divider value a)
  SetFrequency(results);
  delay(5000);
  
  Serial1.println(F("END"));
  digitalWrite(SI5_POWER, LOW);
  delay(5000);
  resetFunc(); //reboot
}


void loop()
{
  
}

@maqifrnswa
Copy link
Author

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
reset after the first time calcDivFast is called. Or you can use the same MS divider as used in the KW method (rather than passing it a 0 the first time it was called). That should help maintain the same intermediate frequency and avoid the need for a PLL reset.

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.

@KevWal
Copy link

KevWal commented Mar 20, 2021

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!)

https://pastebin.com/ZQmT0hZq

and outputs this:

13:29:13.860 -> si5351a powered on and I2C Initialised.
13:29:18.882 -> Reg 0: 0 17 Reg 1: 0 16 Reg 2: 0 3
13:29:18.882 -> Set Initial frequency and reset PLL's
13:29:18.916 -> SetFrequency():
13:29:18.916 -> FB_a 33
13:29:18.916 -> FB_b 194180
13:29:18.916 -> FB_c 1048575
13:29:18.916 -> FB_int 0
13:29:18.916 -> MS_a 64
13:29:18.916 -> MS_b 0
13:29:18.916 -> MS_c 1
13:29:18.916 -> MS_int 0
13:29:23.965 -> Set Initial frequency and reset PLL's
13:29:23.965 -> SetFrequency():
13:29:23.965 -> FB_a 33
13:29:23.965 -> FB_b 5
13:29:23.965 -> FB_c 27
13:29:23.965 -> FB_int 0
13:29:23.965 -> MS_a 66
13:29:24.000 -> MS_b 0
13:29:24.000 -> MS_c 1
13:29:24.000 -> MS_int 1
13:29:24.034 -> Reg 0: 0 17 Reg 1: 0 16 Reg 2: 0 3

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants