Last active
October 3, 2022 12:08
-
-
Save jepler/e2354d64009948c041f6610468cdf655 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# The MIT License (MIT) | |
# | |
# Copyright (c) 2020 Jeff Epler | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in | |
# all copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
# THE SOFTWARE. | |
import argparse | |
import fractions | |
import functools | |
import math | |
# Find all the ways to partition 'seq' into two subsequences a and b. | |
# Arbitrarily, the first item is always in the 'a' sequence, so e.g., | |
# the partitions of 2 items [p, q] are just 2: [[p], [q]] and [[p, q], []]. | |
# the partitions [[q], [p]] and [[], [p, q]] will not be returned. | |
# | |
# The empty sequence results in the single partition [[], []] | |
def partitions(seq): | |
seq = list(seq) | |
# Special case: empty sequence | |
if len(seq) == 0: | |
yield [], [] | |
else: | |
for j in range(1, 2**len(seq), 2): | |
yield ( | |
[si for i, si in enumerate(seq) if j & (1<<i)], | |
[si for i, si in enumerate(seq) if (~j) & (1<<i)] | |
) | |
# Convert various representations to fractions | |
# besides what fractions.Fraction will parse, you can write | |
# - underscores within numbers, as place separators | |
# 1_000_000 | |
# - Mixed fractions | |
# 1+3/4 or 1 3/4 (both equal to 7/4) | |
# or 1-3/4 (equal to 1/4) | |
# - Exponentials in any part | |
# 1e3 5_000e3/1e9 | |
# - Common suffixes: M, G, K, m, u | |
def F(n): | |
if isinstance(n, str): | |
n = n.replace("_", "") | |
n = n.replace("+", " ") | |
n = n.replace("-", " -") | |
n = n.replace("m", "e-3") | |
n = n.replace("u", "e-6") | |
n = n.replace("k", "e3") | |
n = n.replace("M", "e6") | |
n = n.replace("G", "e9") | |
if ' ' in n: # Accomodate 1 1/3, 1+1/3 | |
w, n = n.rsplit(None, 1) | |
else: | |
w = 0 | |
if '/' in n: | |
n, d = n.split('/') | |
else: | |
d = 1 | |
return F(w) + fractions.Fraction(n) / fractions.Fraction(d) | |
return fractions.Fraction(n) | |
def flcm(a, b): | |
a = F(a) | |
b = F(b) | |
p = a.numerator * b.denominator | |
q = b.numerator * a.denominator | |
r = math.gcd(p, q) | |
d = a.denominator * b.denominator | |
return fractions.Fraction(p * q, r * d) | |
def err_str(x): | |
if x == 0: return "0" | |
if x < 1e-12: return "%g" % x | |
if x < 1e-9: return "%.3fppt" % (x * 1e12) | |
if x < 1e-6: return "%.3fppb" % (x * 1e9) | |
return "%.3fppm" % (x * 1e6) | |
def place_in_range(freq, f_low): | |
while freq < f_low / 2048: | |
freq *= 2 | |
return freq | |
def ilog2(x): | |
j = 0 | |
while x > 1: | |
j += 1 | |
x /= 2 | |
return j | |
def calculate_freq(clocks, f_low, f_high): | |
freq = functools.reduce(flcm, clocks, 1) | |
n = (f_low + freq - 1) // freq | |
r = freq * n | |
return r | |
( | |
INTEGER_MULTIPLIER_DOUBLE_INTEGER_DIVISOR, | |
FRACTIONAL_MULTIPLIER_DOUBLE_INTEGER_DIVISOR, | |
INTEGER_MULTIPLIER_INTEGER_DIVISOR, | |
FRACTIONAL_MULTIPLIER_INTEGER_DIVISOR, | |
INTEGER_MULTIPLIER_FRACTIONAL_DIVISOR, | |
FRACTIONAL_MULTIPLIER_FRACTIONAL_DIVISOR, | |
) = range(6) | |
plan_names = { | |
INTEGER_MULTIPLIER_DOUBLE_INTEGER_DIVISOR: "Integer multiplier, double integer divisor", | |
FRACTIONAL_MULTIPLIER_DOUBLE_INTEGER_DIVISOR: "Fractional multiplier, double integer divisor", | |
INTEGER_MULTIPLIER_INTEGER_DIVISOR: "Integer multiplier, integer divisior", | |
FRACTIONAL_MULTIPLIER_INTEGER_DIVISOR: "Fractional multiplier, integer divisor", | |
INTEGER_MULTIPLIER_FRACTIONAL_DIVISOR: "Integer multiplier, fractional divisor", | |
FRACTIONAL_MULTIPLIER_FRACTIONAL_DIVISOR: "Fractional multiplier, fractional divisor", | |
} | |
MAX_DENOM = 1048575 | |
class Pll: | |
def __init__(self, f_in, clocks, *, f_low=600_000_000, f_high=900_000_000): | |
self.f_in = F(f_in) | |
self.f_low = F(f_low) | |
self.f_high = F(f_high) | |
self.clocks = [F(c) for c in clocks] | |
self.setting_type, self.multiplier = self._calculate() | |
def _calculate(self): | |
f_in = self.f_in | |
clocks = [place_in_range(c, self.f_low) for c in self.clocks] | |
clocks2 = [2*c for c in clocks] | |
f = calculate_freq(clocks2 + [f_in], self.f_low, self.f_high) | |
if f < self.f_high: | |
return INTEGER_MULTIPLIER_DOUBLE_INTEGER_DIVISOR, f / f_in | |
clocks = [2*c for c in clocks] | |
f = calculate_freq(clocks2, self.f_low, self.f_high) | |
if f < self.f_high and (f / f_in).denominator <= MAX_DENOM: | |
return FRACTIONAL_MULTIPLIER_DOUBLE_INTEGER_DIVISOR, f / f_in | |
f = calculate_freq(clocks + [f_in], self.f_low, self.f_high) | |
if f < self.f_high: | |
return INTEGER_MULTIPLIER_INTEGER_DIVISOR, f / f_in | |
while True: | |
clocks.pop() | |
if not clocks: | |
break | |
f = calculate_freq(clocks + [f_in], self.f_low, self.f_high) | |
if f < self.f_high: | |
return INTEGER_MULTIPLIER_INTEGER_DIVISOR, f / f_in | |
f = calculate_freq(clocks, self.f_low, self.f_high) | |
if f < self.f_high and (f / f_in).denominator <= MAX_DENOM: | |
return FRACTIONAL_MULTIPLIER_FRACTIONAL_DIVISOR, f / f_in | |
ratio = (self.f_low / f_in).limit_denominator(MAX_DENOM) | |
return FRACTIONAL_MULTIPLIER_FRACTIONAL_DIVISOR, ratio | |
def exact_divider(self, clock): | |
clock2 = place_in_range(clock, self.f_low) | |
return ((self.f_out / clock2), ilog2(clock2 / clock)) | |
def divider(self, clock): | |
de, r = self.exact_divider(clock) | |
d = de.limit_denominator(MAX_DENOM) | |
return d, r | |
def error(self, clock): | |
de, r = self.exact_divider(clock) | |
d = de.limit_denominator(MAX_DENOM) | |
return (de - d) / de | |
@property | |
def f_out(self): | |
return self.f_in * self.multiplier | |
@property | |
def dividers(self): | |
return [self.divider(c) for c in self.clocks] | |
@property | |
def errors(self): | |
return [self.error(c) for c in self.clocks] | |
@property | |
def score(self): | |
if not self.clocks: return 0 | |
e = len([e for e in self.errors if e == 0]) | |
did = len([d for d, r in self.dividers if d.denominator == 1 and d.numerator % 2 == 0]) | |
sid = len([d for d, r in self.dividers if d.denominator == 1]) | |
lc = len(self.clocks) | |
return 6-self.setting_type + fractions.Fraction(e, lc) + fractions.Fraction(did, lc*lc) + fractions.Fraction(did, lc**3) | |
def print(self): | |
print(f"Frequency plan type: {plan_names[self.setting_type]} ({self.setting_type})") | |
print(f"Multiplier = {self.multiplier} ({float(self.multiplier)})") | |
print(f"Intermediate frequency = {self.f_out} ({float(self.f_out)})") | |
print() | |
for c in self.clocks: | |
d, r = self.divider(c) | |
e = self.error(c) | |
print(f"Desired output frequency: {c} ({float(c)})") | |
print(f"Divider = {d} ({float(d)})") | |
if e == 0: | |
print("Exact") | |
else: | |
c_actual = self.f_out / d / (2**r) | |
print(f"Actual output frequency: {c_actual} ({float(c_actual)})") | |
print(f"Relative Error: {err_str(e)}") | |
print(f"Absolute Error: {float(c - c_actual):.3g}Hz") | |
print(f"r_divider = {r} (/ {2**r})") | |
print() | |
def plan(f_in, c1, c2): | |
p = Pll(f_in, c1) | |
q = Pll(f_in, c2) | |
return (p.score + q.score, p, q) | |
parser = argparse.ArgumentParser( | |
description='Create a frequency plan for Si5351') | |
parser.add_argument('frequencies', metavar='freq', nargs='+', | |
type=F, help='Integer, ratio, or decimal frequencies') | |
parser.add_argument('--input-frequency', '-i', metavar='freq', | |
default=25000000, type=F, | |
help='Input frequency') | |
args = parser.parse_args() | |
f_in = args.input_frequency | |
score, p, q = max((plan(f_in, p, q) for p, q in partitions(args.frequencies)), key=lambda s: s[0]) | |
print(f"Input frequency: {f_in} ({float(f_in)})") | |
print(f"Frequency plan score: {float(score):.2f}") | |
print() | |
print("PLL A:") | |
p.print() | |
if q.clocks: | |
print() | |
print("PLL B:") | |
q.print() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment