108 lines
3.5 KiB
Python
108 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
2nd-order unity-gain Butterworth low-pass (Sallen-Key design) calculator + Bode plots.
|
|
|
|
- Computes R1=R2=R (user-chosen) and C1,C2 from TI normalized values:
|
|
C1n=1.414, C2n=0.707, with scaling C = Cn/(R*ω0), ω0=2πfc
|
|
|
|
- Plots magnitude (dB) and phase (deg) of the ideal 2nd-order Butterworth:
|
|
H(s) = ω0^2 / (s^2 + (ω0/Q)s + ω0^2), Q=1/√2
|
|
|
|
Usage:
|
|
python3 butterworth_sallenkey_bode.py 10000 --R 10000
|
|
"""
|
|
|
|
import argparse
|
|
import math
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
|
|
C1N = 1.414
|
|
C2N = 0.707
|
|
Q_BUTTERWORTH = 1.0 / math.sqrt(2.0)
|
|
|
|
def format_si(x, unit=""):
|
|
if x == 0:
|
|
return f"0 {unit}".strip()
|
|
prefixes = [
|
|
(1e-12, "p"), (1e-9, "n"), (1e-6, "u"), (1e-3, "m"),
|
|
(1, ""), (1e3, "k"), (1e6, "M"), (1e9, "G")
|
|
]
|
|
ax = abs(x)
|
|
# choose prefix so scaled value is ~[1..1000)
|
|
best = min(prefixes, key=lambda p: abs(math.log10(ax/p[0])))
|
|
scale, pref = best
|
|
return f"{x/scale:.4g} {pref}{unit}".strip()
|
|
|
|
def butterworth_H(jw, w0, Q=Q_BUTTERWORTH):
|
|
# H(jw) = w0^2 / ( (jw)^2 + (w0/Q)*(jw) + w0^2 )
|
|
s = 1j * jw
|
|
return (w0**2) / (s**2 + (w0/Q)*s + w0**2)
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Butterworth Sallen-Key LPF (2nd order) calculator + Bode plotter.")
|
|
ap.add_argument("fc", type=float, help="Cutoff frequency (Hz), e.g. 10000")
|
|
ap.add_argument("--R", type=float, default=10_000.0, help="Choose R1=R2=R in ohms (default 10000)")
|
|
ap.add_argument("--fmin", type=float, default=None, help="Plot min frequency (Hz). Default fc/100")
|
|
ap.add_argument("--fmax", type=float, default=None, help="Plot max frequency (Hz). Default fc*100")
|
|
ap.add_argument("--points", type=int, default=2000, help="Number of frequency points (default 2000)")
|
|
ap.add_argument("--save", type=str, default=None, help="Save plot to PNG path instead of showing it")
|
|
args = ap.parse_args()
|
|
|
|
fc = args.fc
|
|
R = args.R
|
|
w0 = 2.0 * math.pi * fc
|
|
|
|
# Component calculation (ideal per TI normalized values)
|
|
C1 = C1N / (R * w0)
|
|
C2 = C2N / (R * w0)
|
|
|
|
print("2nd-order unity-gain Butterworth Sallen-Key LPF (ideal)")
|
|
print(f"fc = {format_si(fc, 'Hz')}")
|
|
print(f"R1=R2= {format_si(R, 'ohm')}")
|
|
print(f"C1 = {format_si(C1, 'F')} (norm 1.414)")
|
|
print(f"C2 = {format_si(C2, 'F')} (norm 0.707)")
|
|
print(f"Q = {Q_BUTTERWORTH:.6g}")
|
|
print()
|
|
|
|
# Frequency axis for Bode plot
|
|
fmin = args.fmin if args.fmin is not None else fc / 100.0
|
|
fmax = args.fmax if args.fmax is not None else fc * 100.0
|
|
if fmin <= 0:
|
|
fmin = fc / 100.0
|
|
|
|
f = np.logspace(np.log10(fmin), np.log10(fmax), args.points)
|
|
w = 2.0 * np.pi * f
|
|
|
|
H = butterworth_H(w, w0, Q_BUTTERWORTH)
|
|
mag_db = 20.0 * np.log10(np.abs(H))
|
|
phase_deg = np.unwrap(np.angle(H)) * 180.0 / np.pi
|
|
|
|
# Plot
|
|
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9, 7), sharex=True)
|
|
|
|
ax1.semilogx(f, mag_db)
|
|
ax1.axvline(fc, linestyle="--", linewidth=1)
|
|
ax1.axhline(-3.0, linestyle="--", linewidth=1)
|
|
ax1.set_ylabel("Magnitude (dB)")
|
|
ax1.grid(True, which="both", linestyle=":")
|
|
|
|
ax2.semilogx(f, phase_deg)
|
|
ax2.axvline(fc, linestyle="--", linewidth=1)
|
|
ax2.set_ylabel("Phase (deg)")
|
|
ax2.set_xlabel("Frequency (Hz)")
|
|
ax2.grid(True, which="both", linestyle=":")
|
|
|
|
fig.suptitle(f"2nd-order Butterworth LPF (fc={fc:g} Hz, R={R:g} Ω)")
|
|
fig.tight_layout()
|
|
|
|
if args.save:
|
|
plt.savefig(args.save, dpi=200)
|
|
print(f"Saved plot to: {args.save}")
|
|
else:
|
|
plt.show()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|