board
Diode-Bridge Voltage-Controlled Attenuator
Public Unreviewedby John Lauer
Two-layer through-hole audio attenuator: four 1N4148s as a current-biased shunt bridge, 0 to 2 V control, designed by Saif Abdessayed with Adom Hydrogen
main
John Lauer
Saif Abdessayed's diode-bridge VCA: KiCad project, Gerbers, BOM, simulations, reports, and the earlier RC filter test
45f3ff6
11d ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
#!/usr/bin/env python3
from pathlib import Path
import csv, re, subprocess
HERE = Path(__file__).resolve().parent
DECKS = HERE / "decks"
RESULTS = HERE / "results"
COMMON = (DECKS / "common.cir.inc").read_text()
RESULTS.mkdir(exist_ok=True)
def run(name, tail, params):
deck = COMMON
for key, value in params.items():
deck = re.sub(rf"\.param {key}=[^ \n]+", f".param {key}={value}", deck)
deck += "\n" + tail + "\n.end\n"
path = DECKS / f"{name}.cir"
log = RESULTS / f"{name}.log"
path.write_text(deck)
p = subprocess.run(["ngspice", "-b", "-o", str(log), str(path)], text=True, capture_output=True)
if p.returncode:
raise SystemExit(f"{name}: ngspice failed: {p.stderr}")
return log.read_text(errors="replace")
rows = []
for vc in (0, 0.5, 1, 1.5, 2):
name = f"small_vc_{str(vc).replace('.', 'p')}"
text = run(name, """.tran 2u 30m 10m
.measure tran vout_pp PP v(out) from=20m to=30m
.measure tran vin_pp PP v(in) from=20m to=30m
.measure tran gain param='vout_pp/vin_pp'
.four 1k v(out)""", {"VC": vc, "VINPP": 0.1, "FREQ": "1k"})
vals = dict(re.findall(r"^(vout_pp|vin_pp|gain)\s*=\s*([-+0-9.eE]+)", text, re.M))
thd = re.search(r"THD:\s*([0-9.eE+-]+) %", text)
rows.append({"control_v": vc, "vin_vpp": 0.1, "vout_vpp": float(vals["vout_pp"]), "gain": float(vals["gain"]), "thd_percent": float(thd.group(1)) if thd else None})
for vc in (0, 0.5, 1, 1.5, 2):
name = f"large_vc_{str(vc).replace('.', 'p')}"
text = run(name, """.tran 1u 30m 10m
.measure tran vout_pp PP v(out) from=20m to=30m
.measure tran vin_pp PP v(in) from=20m to=30m
.measure tran gain param='vout_pp/vin_pp'
.four 1k v(out)""", {"VC": vc, "VINPP": 12, "FREQ": "1k"})
vals = dict(re.findall(r"^(vout_pp|vin_pp|gain)\s*=\s*([-+0-9.eE]+)", text, re.M))
thd = re.search(r"THD:\s*([0-9.eE+-]+) %", text)
rows.append({"control_v": vc, "vin_vpp": 12, "vout_vpp": float(vals["vout_pp"]), "gain": float(vals["gain"]), "thd_percent": float(thd.group(1)) if thd else None})
with (RESULTS / "transient_summary.csv").open("w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys()); w.writeheader(); w.writerows(rows)
freq_rows = []
for vc in (0, 0.5, 1, 1.5, 2):
name = f"ac_vc_{str(vc).replace('.', 'p')}"
text = run(name, """.ac dec 200 20 20k
.print ac vm(out)
.measure ac gain20 FIND vm(out) AT=20
.measure ac gain1k FIND vm(out) AT=1k
.measure ac gain20k FIND vm(out) AT=20k""", {"VC": vc, "VINPP": 0.1, "FREQ": "1k"})
vals = dict(re.findall(r"^(gain20|gain1k|gain20k)\s*=\s*([-+0-9.eE]+)", text, re.M))
import math
freq_rows.append({"control_v": vc, "gain_db_20hz": 20*math.log10(float(vals["gain20"])), "gain_db_1khz": 20*math.log10(float(vals["gain1k"])), "gain_db_20khz": 20*math.log10(float(vals["gain20k"]))})
with (RESULTS / "frequency_summary.csv").open("w", newline="") as f:
w = csv.DictWriter(f, fieldnames=freq_rows[0].keys()); w.writeheader(); w.writerows(freq_rows)
# Worst-case-style feedthrough model: D4 Is is 2% high and control rises in 1 ms.
mismatch = COMMON.replace("D4 brb brn D4148A", "D4 brb brn D4148B").replace("VCTRL ctrl 0 {VC}", "VCTRL ctrl 0 PWL(0 0 9m 0 10m 2 40m 2)")
mismatch = mismatch.replace("VIN in 0 SIN(0 {VINPP/2} {FREQ}) AC 1", "VIN in 0 0")
tail = """.tran 2u 40m
.print tran v(out)
.measure tran ftmax MAX v(out) from=9m to=25m
.measure tran ftmin MIN v(out) from=9m to=25m"""
path = DECKS / "control_feedthrough_mismatch.cir"
path.write_text(mismatch + "\n" + tail + "\n.end\n")
log = RESULTS / "control_feedthrough_mismatch.log"
subprocess.run(["ngspice", "-b", "-o", str(log), str(path)], check=True)
feed_text = log.read_text()
ftmax = float(re.search(r"ftmax\s*=\s*([-+0-9.eE]+)", feed_text, re.M).group(1))
ftmin = float(re.search(r"ftmin\s*=\s*([-+0-9.eE]+)", feed_text, re.M).group(1))
(RESULTS / "control_feedthrough_summary.txt").write_text(f"2% D4 Is mismatch, 0-to-2 V / 1 ms control ramp: peak |Vout| = {max(abs(ftmax), abs(ftmin)):.6g} V\n")
print("simulation complete")