I improved the PLL calculator so that it acts numerically perfect now:

xinfreq = 20000000
clkfreq = 333333333
errfreq = 100000
error = errfreq
for pppp = 0 to 15
if pppp = 0 then post = 1 else post = pppp * 2
for divd = 64 to 1 step -1
Fpfd = round(xinfreq / divd)
mult = round(clkfreq * (post * divd) / xinfreq)
Fvco = round(xinfreq * mult / divd)
Fout = round(Fvco / post)
e = abs(Fout - clkfreq)
if (e <= error) and (Fpfd >= 250000) and (mult <= 1024) and (Fvco >= 99e6) and ((Fvco <= 201e6) or (Fvco <= clkfreq + errfreq)) then
result_divd = divd
result_mult = mult
result_post = post
result_pppp = (pppp-1) & 15 '%1111 = /1, %0000..%1110 = /2..30
result_Fpfd = Fpfd
result_Fvco = Fvco
result_Fout = Fout
error = e
endif
next
next
print "XINFREQ: "; xinfreq, " (XI Input Frequency)"
print "CLKFREQ: "; clkfreq, " (PLL Goal Frequency)"
print
print "Divd: "; result_divd, "D field = "; result_divd-1
print "Mult: "; result_mult, "M field = "; result_mult-1
print "Post: "; result_post, "P field = "; result_pppp
print
print "setclk(%1_";
print right(bin(result_divd-1),6); "_";
print right(bin(result_mult-1),10); "_";
print right(bin(result_pppp),4); "_10_11, "; clkfreq; ")"
print
print "Fpfd: "; result_Fpfd
print "Fvco: "; result_Fvco
print
print "Fout: "; result_Fout
print "Error: "; result_Fout - clkfreq

I've remembered that I wanted to make this into a runtime Pasm library function. Just completed transcribing it directly. Looking good so far. Next is to compact it down and comment with more than just the pseudo code.

Anyone care to do a Spin version? I struggle with lack of spin knowledge.

I did my own Spin2 version here and I already use it as part of my video driver. It works out the clock mode for a desired frequency or returns 0 if it could not meet the tolerance. The only annoying part is figuring out the bottom bits of the clock mode automatically. I sort of need to provide that in extra constants which may be duplicating some SPIN2 constants (and hence prone to inconsistency + mismatch) because I'm not sure this information can be accessed the way I wanted in the lower level object.

CON
' clock source used below
#0, CLKSRC_XTAL, CLKSRC_XIN
' setup one of these based on your P2 HW input clock,
' this will only be used if the PLL settings get automatically computed (see code below)
'CLKIN_HZ = _xtalfreq ' also only enable CLKSRC_XTAL below as CLKSRC
'CLKIN_HZ = _xinfreq ' also only enable CLKSRC_XIN below as CLKSRC
CLKIN_HZ = 20000000 ' assume 20MHz crystal by default
CLKSRC = CLKSRC_XTAL ' enable this for crystal clock source (default)
'CLKSRC = CLKSRC_XIN ' enable this for direct input clock source on XI (no crystal)
' parameters used when automatically determining PLL settings
TOLERANCE_HZ = 500000 ' pixel clock accuracy will be constrained by this when no exact ratios are found
MAXVCO_HZ = 350000000 ' for safety, but you could try to overclock even higher at your own risk
MINVCO_HZ = 100000000
MINPLLIN_HZ = 500000 ' setting lower can find more PLL ratios but may begin to introduce more PLL jitter
PUB computeClockMode(desiredHz) : mode | vco, finput, f, p, div, m, error, bestError
bestError := -1
repeat p from 0 to 30 step 2
' compute the ideal VCO frequency f at this value of P
if p <> 0
if desiredHz > MAXVCO_HZ/p ' test it like this to not overflow
quit
f := desiredHz * p
else
f := desiredHz
if f > MAXVCO_HZ
quit
' scan through D values, and find best M, retain best case
repeat div from 1 to 64
'compute the PLL input frequency from the crystal through the divider
finput := CLKIN_HZ/div
if finput < MINPLLIN_HZ ' input getting too low, and only gets lower so quit now
quit
' determine M value needed for this ideal VCO frequency and input frequency
m := f / finput
' check for the out of divider range case
if m +> 1024
quit
' zero is special and gets a second chance
if m == 0
m++
' compute the actual VCO frequency at this particular M, D setting
vco := finput * m
if vco +< MINVCO_HZ
quit
if vco +> MAXVCO_HZ
next
' compute the error and check next higher M value if possible, it may be closer
error := abs(f - vco)
if m < 1024 and (vco + finput) +< MAXVCO_HZ
if error > abs(f - (vco + finput))
error := abs(f - (vco + finput))
m++
' retain best allowed frequency error and divider bits found so far
if error +< bestError and error +< TOLERANCE_HZ+1
bestError := error
mode := ((div-1) << 18) + ((m-1) << 8) + (((p/2 - 1) & $f) << 4)
' quit whenever perfect match found
if bestError == 0
quit
if bestError == 0
quit
' final clock mode format is this #%0000_000E_DDDD_DDMM_MMMM_MMMM_PPPP_CCSS
if mode
' also set 15 or 30pF capacitor loading based on input crystal frequency
mode |= (1<<24) ' enable PLL
if (CLKSRC == CLKSRC_XTAL) ' enable oscillator and caps for crystal
mode |= (CLKIN_HZ < 16000000) ? %1111 : %1011
else
mode |= %0111 ' don't enable oscillator

Here's what I've been using historically. It's been very useful for my testing. It uses only XMUL (For the M field) as an input variable so makes it a small routine. The other components are all constants.

'recalculate sysclock hertz using cordic
qmul xtalmul, ##(XTALFREQ / XDIV / XDIVP) 'integer component of pre-divided crystal frequency
mov pa, xtalmul
mul pa, ##round((float(XDIV*XDIVP)+0.5) * (float(XTALFREQ)/float(XDIV)/float(XDIVP) - float(XTALFREQ/XDIV/XDIVP)))
qdiv pa, ##(XDIV * XDIVP) 'fractional component of pre-divided crystal frequency
getqx clk_freq 'result of integer component
getqx pa 'result of fractional component
add clk_freq, pa 'de-error the integer rounding

It doesn't compute the clock mode because XMUL is the only component to adjust. So it goes in the opposite direction of computing the sys-clock frequency from the given mode components. This is then needed for further computing timing parameters like for the serial pins for example.

## Comments

10,833I've remembered that I wanted to make this into a runtime Pasm library function. Just completed transcribing it directly. Looking good so far. Next is to compact it down and comment with more than just the pseudo code.

Anyone care to do a Spin version? I struggle with lack of spin knowledge.

3,14710,83310,833It doesn't compute the clock mode because XMUL is the only component to adjust. So it goes in the opposite direction of computing the sys-clock frequency from the given mode components. This is then needed for further computing timing parameters like for the serial pins for example.